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.
Files changed (89) hide show
  1. package/README.md +46 -5
  2. package/dist/arb/reconcile.d.ts +28 -0
  3. package/dist/arb/reconcile.js +162 -0
  4. package/dist/arb/sizing.d.ts +12 -1
  5. package/dist/arb/sizing.js +38 -5
  6. package/dist/arb/state.d.ts +6 -0
  7. package/dist/cli-spec.d.ts +0 -4
  8. package/dist/cli-spec.js +8 -1
  9. package/dist/commands/account.d.ts +1 -1
  10. package/dist/commands/account.js +300 -63
  11. package/dist/commands/agent.js +16 -9
  12. package/dist/commands/arb/index.js +243 -49
  13. package/dist/commands/arb-auto.js +696 -9
  14. package/dist/commands/arb.d.ts +2 -1
  15. package/dist/commands/arb.js +535 -131
  16. package/dist/commands/bot.js +207 -0
  17. package/dist/commands/dex.js +7 -1
  18. package/dist/commands/funding.d.ts +2 -1
  19. package/dist/commands/funding.js +211 -4
  20. package/dist/commands/funds.d.ts +4 -0
  21. package/dist/commands/funds.js +741 -0
  22. package/dist/commands/history.d.ts +2 -1
  23. package/dist/commands/history.js +508 -5
  24. package/dist/commands/manage.js +6 -4
  25. package/dist/commands/market.js +35 -2
  26. package/dist/commands/multileg.d.ts +16 -0
  27. package/dist/commands/multileg.js +174 -0
  28. package/dist/commands/pnl.d.ts +12 -0
  29. package/dist/commands/pnl.js +243 -0
  30. package/dist/commands/risk.d.ts +2 -0
  31. package/dist/commands/risk.js +56 -0
  32. package/dist/commands/settings.js +147 -0
  33. package/dist/commands/stream.js +132 -55
  34. package/dist/commands/trade.d.ts +1 -1
  35. package/dist/commands/trade.js +270 -267
  36. package/dist/commands/wallet.d.ts +4 -0
  37. package/dist/commands/wallet.js +11 -1
  38. package/dist/config.d.ts +6 -1
  39. package/dist/config.js +21 -1
  40. package/dist/dashboard/ws-feeds.js +1 -1
  41. package/dist/equity-tracker.d.ts +57 -0
  42. package/dist/equity-tracker.js +209 -0
  43. package/dist/exchanges/hyperliquid-spot.d.ts +69 -0
  44. package/dist/exchanges/hyperliquid-spot.js +309 -0
  45. package/dist/exchanges/hyperliquid.d.ts +26 -5
  46. package/dist/exchanges/hyperliquid.js +171 -73
  47. package/dist/exchanges/interface.d.ts +1 -1
  48. package/dist/exchanges/lighter-spot.d.ts +55 -0
  49. package/dist/exchanges/lighter-spot.js +415 -0
  50. package/dist/exchanges/lighter.d.ts +10 -2
  51. package/dist/exchanges/lighter.js +210 -56
  52. package/dist/exchanges/pacifica.d.ts +11 -4
  53. package/dist/exchanges/pacifica.js +64 -37
  54. package/dist/exchanges/spot-interface.d.ts +45 -0
  55. package/dist/exchanges/spot-interface.js +22 -0
  56. package/dist/execution-log.d.ts +2 -2
  57. package/dist/funding/rates.d.ts +36 -0
  58. package/dist/funding/rates.js +146 -0
  59. package/dist/index.js +126 -161
  60. package/dist/liquidity.d.ts +12 -0
  61. package/dist/liquidity.js +43 -0
  62. package/dist/mcp-server.js +43 -96
  63. package/dist/signer/evm-local.d.ts +12 -0
  64. package/dist/signer/evm-local.js +19 -0
  65. package/dist/signer/index.d.ts +3 -0
  66. package/dist/signer/index.js +2 -0
  67. package/dist/signer/interface.d.ts +31 -0
  68. package/dist/signer/interface.js +7 -0
  69. package/dist/signer/solana-local.d.ts +13 -0
  70. package/dist/signer/solana-local.js +19 -0
  71. package/dist/smart-order.js +14 -3
  72. package/dist/strategies/split-order.d.ts +49 -0
  73. package/dist/strategies/split-order.js +138 -0
  74. package/dist/utils.js +7 -1
  75. package/dist/ws/market-feed.d.ts +74 -0
  76. package/dist/ws/market-feed.js +227 -0
  77. package/package.json +1 -1
  78. package/skills/perp-cli/SKILL.md +80 -4
  79. package/skills/perp-cli/references/agent-operations.md +19 -9
  80. package/skills/perp-cli/references/commands.md +54 -32
  81. package/skills/perp-cli/references/strategies.md +27 -17
  82. package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
  83. package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
  84. package/skills/perp-cli/scripts/preflight.sh +98 -0
  85. package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
  86. package/skills/perp-cli/scripts/validate-arb.sh +112 -0
  87. package/skills/perp-cli/templates/arb-scan.sh +0 -15
  88. package/skills/perp-cli/templates/open-position.sh +0 -38
  89. package/skills/perp-cli/templates/wallet-setup.sh +0 -17
@@ -3,6 +3,12 @@ import { printJson, jsonOk } from "../utils.js";
3
3
  import { loadBotConfig, quickGridConfig, quickDCAConfig } from "../bot/config.js";
4
4
  import { runBot } from "../bot/engine.js";
5
5
  import { PRESETS, getPreset, getPresetsByStrategy } from "../bot/presets.js";
6
+ import { updateJobState } from "../jobs.js";
7
+ import { runTWAP } from "../strategies/twap.js";
8
+ import { runFundingArb } from "../strategies/funding-arb.js";
9
+ import { runGrid } from "../strategies/grid.js";
10
+ import { runDCA } from "../strategies/dca.js";
11
+ import { runTrailingStop } from "../strategies/trailing-stop.js";
6
12
  export function registerBotCommands(program, getAdapter, getAdapterFor, isJson) {
7
13
  const bot = program.command("bot").description("Automated trading bots with condition monitoring & risk management");
8
14
  // ── bot start <config> ──
@@ -326,6 +332,8 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
326
332
  console.log(chalk.gray(` Usage: perp bot start ~/.perp/bots/eth-grid.yaml`));
327
333
  console.log(chalk.gray(` perp bot start ~/.perp/bots/eth-grid.yaml --background\n`));
328
334
  });
335
+ // Strategy subcommands: twap, funding-arb, grid, dca, trailing-stop
336
+ registerRunSubcommands(bot, getAdapter, getAdapterFor, isJson);
329
337
  }
330
338
  function makeLog() {
331
339
  return (msg) => {
@@ -410,3 +418,202 @@ const ARB_EXAMPLE = chalk.gray(` name: funding-arb
410
418
  risk:
411
419
  max_drawdown: 200
412
420
  max_daily_loss: 50`);
421
+ // ── Run subcommands (twap, funding-arb, grid, dca, trailing-stop) ──
422
+ function registerRunSubcommands(parent, getAdapter, getAdapterFor, isJson) {
423
+ const run = parent;
424
+ const twapCmd = run
425
+ .command("twap <symbol> <side> <size> <duration>")
426
+ .description("Run client-side TWAP (splits market orders over time)");
427
+ twapCmd
428
+ .option("-s, --slices <n>", "Number of slices (default: auto)")
429
+ .option("--job-id <id>", "Job ID (set automatically for background jobs)")
430
+ .action(async (symbol, side, size, duration, opts) => {
431
+ const s = side.toLowerCase();
432
+ if (s !== "buy" && s !== "sell") {
433
+ console.error(chalk.red("Side must be buy or sell"));
434
+ process.exit(1);
435
+ }
436
+ const adapter = await getAdapter();
437
+ const params = {
438
+ symbol: symbol.toUpperCase(),
439
+ side: s,
440
+ totalSize: parseFloat(size),
441
+ durationSec: parseInt(duration),
442
+ slices: opts.slices ? parseInt(opts.slices) : undefined,
443
+ };
444
+ const log = (msg) => {
445
+ const ts = new Date().toLocaleTimeString();
446
+ console.log(`${chalk.gray(ts)} ${msg}`);
447
+ };
448
+ const result = await runTWAP(adapter, params, opts.jobId, log);
449
+ if (isJson())
450
+ printJson(jsonOk(result));
451
+ if (opts.jobId) {
452
+ updateJobState(opts.jobId, { status: "done" });
453
+ }
454
+ });
455
+ run
456
+ .command("funding-arb")
457
+ .description("Run funding rate arbitrage monitor/executor")
458
+ .option("--min-spread <pct>", "Min annual spread % to enter", "10")
459
+ .option("--close-spread <pct>", "Close when spread drops below (%)", "5")
460
+ .option("--size <size>", "Position size per leg (base amount)", "0.01")
461
+ .option("--size-usd <usd>", "Position size per leg in USD (alternative)")
462
+ .option("--symbols <list>", "Comma-separated symbol filter")
463
+ .option("--interval <sec>", "Check interval in seconds", "60")
464
+ .option("--auto-execute", "Actually place trades (default: monitor only)")
465
+ .option("--max-positions <n>", "Max simultaneous positions", "3")
466
+ .option("--exchanges <list>", "Comma-separated exchanges to use", "pacifica,hyperliquid,lighter")
467
+ .option("--auto-rebalance", "Trigger rebalance alerts when exchange balance is low")
468
+ .option("--rebalance-threshold <usd>", "Rebalance when available < this ($)", "100")
469
+ .option("--max-drawdown <usd>", "Close all positions if total uPnL exceeds this loss ($)")
470
+ .option("--job-id <id>", "Job ID (set automatically for background jobs)")
471
+ .action(async (opts) => {
472
+ const exchangeNames = opts.exchanges.split(",").map((e) => e.trim());
473
+ const adapters = new Map();
474
+ for (const name of exchangeNames) {
475
+ try {
476
+ const a = await getAdapterFor(name);
477
+ adapters.set(name, a);
478
+ }
479
+ catch {
480
+ if (!isJson())
481
+ console.log(chalk.yellow(` Skipping ${name} (no credentials)`));
482
+ }
483
+ }
484
+ if (adapters.size < 2) {
485
+ console.error(chalk.red("Need at least 2 exchanges for arbitrage. Set credentials for 2+ exchanges."));
486
+ process.exit(1);
487
+ }
488
+ const params = {
489
+ minSpread: parseFloat(opts.minSpread),
490
+ closeSpread: parseFloat(opts.closeSpread),
491
+ size: opts.size,
492
+ sizeUsd: opts.sizeUsd ? parseFloat(opts.sizeUsd) : undefined,
493
+ symbols: opts.symbols ? opts.symbols.split(",").map((s) => s.trim().toUpperCase()) : undefined,
494
+ intervalSec: parseInt(opts.interval),
495
+ autoExecute: opts.autoExecute ?? false,
496
+ maxPositions: parseInt(opts.maxPositions),
497
+ autoRebalance: opts.autoRebalance ?? false,
498
+ rebalanceThreshold: parseFloat(opts.rebalanceThreshold),
499
+ maxDrawdown: opts.maxDrawdown ? parseFloat(opts.maxDrawdown) : undefined,
500
+ };
501
+ const log = (msg) => {
502
+ const ts = new Date().toLocaleTimeString();
503
+ console.log(`${chalk.gray(ts)} ${msg}`);
504
+ };
505
+ await runFundingArb(adapters, params, opts.jobId, log);
506
+ });
507
+ // ── Grid Bot ──
508
+ const gridCmd = run
509
+ .command("grid <symbol>")
510
+ .description("Run grid trading bot (places limit orders across a price range)");
511
+ gridCmd
512
+ .requiredOption("--upper <price>", "Upper price bound")
513
+ .requiredOption("--lower <price>", "Lower price bound")
514
+ .option("--grids <n>", "Number of grid lines", "10")
515
+ .option("--size <size>", "Total position size (base)", "0.1")
516
+ .option("--side <side>", "Grid bias: long, short, neutral", "neutral")
517
+ .option("--leverage <n>", "Leverage to set")
518
+ .option("--interval <sec>", "Check interval in seconds", "10")
519
+ .option("--max-runtime <sec>", "Max runtime in seconds (0 = forever)", "0")
520
+ .option("--trailing-stop <pct>", "Stop if equity drops by this % from peak")
521
+ .option("--job-id <id>", "Job ID (set automatically for background jobs)")
522
+ .action(async (symbol, opts) => {
523
+ const side = opts.side.toLowerCase();
524
+ if (side !== "long" && side !== "short" && side !== "neutral") {
525
+ console.error(chalk.red("Side must be long, short, or neutral"));
526
+ process.exit(1);
527
+ }
528
+ const adapter = await getAdapter();
529
+ const params = {
530
+ symbol: symbol.toUpperCase(),
531
+ side: side,
532
+ upperPrice: parseFloat(opts.upper),
533
+ lowerPrice: parseFloat(opts.lower),
534
+ grids: parseInt(opts.grids),
535
+ totalSize: parseFloat(opts.size),
536
+ leverage: opts.leverage ? parseInt(opts.leverage) : undefined,
537
+ intervalSec: parseInt(opts.interval),
538
+ maxRuntime: parseInt(opts.maxRuntime),
539
+ trailingStop: opts.trailingStop ? parseFloat(opts.trailingStop) : undefined,
540
+ };
541
+ const log = (msg) => {
542
+ const ts = new Date().toLocaleTimeString();
543
+ console.log(`${chalk.gray(ts)} ${msg}`);
544
+ };
545
+ const result = await runGrid(adapter, params, opts.jobId, log);
546
+ if (isJson())
547
+ printJson(jsonOk(result));
548
+ if (opts.jobId) {
549
+ updateJobState(opts.jobId, { status: "done" });
550
+ }
551
+ });
552
+ // ── DCA (Dollar Cost Averaging) ──
553
+ const dcaCmd = run
554
+ .command("dca <symbol> <side> <amount> <interval>")
555
+ .description("Run DCA strategy (periodic market orders at fixed intervals)");
556
+ dcaCmd
557
+ .option("--orders <n>", "Total number of orders (0 = unlimited)", "0")
558
+ .option("--price-limit <price>", "Stop buying above / selling below this price")
559
+ .option("--max-runtime <sec>", "Max runtime in seconds (0 = forever)", "0")
560
+ .option("--job-id <id>", "Job ID (set automatically for background jobs)")
561
+ .action(async (symbol, side, amount, interval, opts) => {
562
+ const s = side.toLowerCase();
563
+ if (s !== "buy" && s !== "sell") {
564
+ console.error(chalk.red("Side must be buy or sell"));
565
+ process.exit(1);
566
+ }
567
+ const adapter = await getAdapter();
568
+ const params = {
569
+ symbol: symbol.toUpperCase(),
570
+ side: s,
571
+ amountPerOrder: parseFloat(amount),
572
+ intervalSec: parseInt(interval),
573
+ totalOrders: parseInt(opts.orders),
574
+ priceLimit: opts.priceLimit ? parseFloat(opts.priceLimit) : undefined,
575
+ maxRuntime: parseInt(opts.maxRuntime),
576
+ };
577
+ const log = (msg) => {
578
+ const ts = new Date().toLocaleTimeString();
579
+ console.log(`${chalk.gray(ts)} ${msg}`);
580
+ };
581
+ const result = await runDCA(adapter, params, opts.jobId, log);
582
+ if (isJson())
583
+ printJson(jsonOk(result));
584
+ if (opts.jobId) {
585
+ updateJobState(opts.jobId, { status: "done" });
586
+ }
587
+ });
588
+ // ── Trailing Stop ──
589
+ const tsCmd = run
590
+ .command("trailing-stop <symbol>")
591
+ .description("Run client-side trailing stop (monitors price, closes position when trail % hit)");
592
+ tsCmd
593
+ .requiredOption("--trail <pct>", "Trail percentage")
594
+ .option("--interval <sec>", "Check interval in seconds", "5")
595
+ .option("--activation <price>", "Only start trailing after price reaches this level")
596
+ .option("--job-id <id>", "Job ID (set automatically for background jobs)")
597
+ .action(async (symbol, opts) => {
598
+ const sym = symbol.toUpperCase();
599
+ const trailPct = parseFloat(opts.trail);
600
+ const intervalSec = parseInt(opts.interval);
601
+ const activationPrice = opts.activation ? parseFloat(opts.activation) : undefined;
602
+ const adapter = await getAdapter();
603
+ const log = (msg) => {
604
+ const ts = new Date().toLocaleTimeString();
605
+ console.log(`${chalk.gray(ts)} ${msg}`);
606
+ };
607
+ const result = await runTrailingStop(adapter, {
608
+ symbol: sym,
609
+ trailPct,
610
+ intervalSec,
611
+ activationPrice,
612
+ }, opts.jobId, log);
613
+ if (isJson())
614
+ printJson(jsonOk(result));
615
+ if (opts.jobId) {
616
+ updateJobState(opts.jobId, { status: "done" });
617
+ }
618
+ });
619
+ }
@@ -1,6 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import { HyperliquidAdapter } from "../exchanges/hyperliquid.js";
3
- import { printJson, jsonOk, makeTable, formatUsd, withJsonErrors } from "../utils.js";
3
+ import { printJson, jsonOk, jsonError, makeTable, formatUsd, withJsonErrors } from "../utils.js";
4
4
  export function registerDexCommands(program, getAdapter, isJson) {
5
5
  const dex = program.command("dex").description("HIP-3 deployed perp dex commands (Hyperliquid)");
6
6
  // ── dex list ──
@@ -11,6 +11,8 @@ export function registerDexCommands(program, getAdapter, isJson) {
11
11
  await withJsonErrors(isJson(), async () => {
12
12
  const adapter = await getAdapter();
13
13
  if (!(adapter instanceof HyperliquidAdapter)) {
14
+ if (isJson())
15
+ return printJson(jsonError("INVALID_EXCHANGE", "HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid."));
14
16
  console.error(chalk.red("\n HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid.\n"));
15
17
  return;
16
18
  }
@@ -41,6 +43,8 @@ export function registerDexCommands(program, getAdapter, isJson) {
41
43
  await withJsonErrors(isJson(), async () => {
42
44
  const adapter = await getAdapter();
43
45
  if (!(adapter instanceof HyperliquidAdapter)) {
46
+ if (isJson())
47
+ return printJson(jsonError("INVALID_EXCHANGE", "HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid."));
44
48
  console.error(chalk.red("\n HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid.\n"));
45
49
  return;
46
50
  }
@@ -80,6 +84,8 @@ export function registerDexCommands(program, getAdapter, isJson) {
80
84
  await withJsonErrors(isJson(), async () => {
81
85
  const adapter = await getAdapter();
82
86
  if (!(adapter instanceof HyperliquidAdapter)) {
87
+ if (isJson())
88
+ return printJson(jsonError("INVALID_EXCHANGE", "HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid."));
83
89
  console.error(chalk.red("\n HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid.\n"));
84
90
  return;
85
91
  }
@@ -1,2 +1,3 @@
1
1
  import { Command } from "commander";
2
- export declare function registerFundingCommands(program: Command, isJson: () => boolean): void;
2
+ import type { ExchangeAdapter } from "../exchanges/interface.js";
3
+ export declare function registerFundingCommands(program: Command, isJson: () => boolean, getAdapterForExchange?: (exchange: string) => Promise<ExchangeAdapter>): void;
@@ -1,9 +1,10 @@
1
1
  import chalk from "chalk";
2
- import { makeTable, formatPercent, formatUsd, printJson, jsonOk } from "../utils.js";
2
+ import { makeTable, formatPercent, formatUsd, formatPnl, printJson, jsonOk, jsonError, withJsonErrors } from "../utils.js";
3
3
  import { fetchAllFundingRates, fetchSymbolFundingRates, TOP_SYMBOLS, } from "../funding-rates.js";
4
- import { annualizeHourlyRate } from "../funding.js";
4
+ import { estimateHourlyFunding, annualizeHourlyRate, annualizeRate, toHourlyRate } from "../funding.js";
5
5
  import { saveFundingSnapshot, getHistoricalRates, getCompoundedAnnualReturn, getExchangeCompoundingHours, } from "../funding-history.js";
6
- export function registerFundingCommands(program, isJson) {
6
+ const ALL_EXCHANGES = ["hyperliquid", "pacifica", "lighter"];
7
+ export function registerFundingCommands(program, isJson, getAdapterForExchange) {
7
8
  const funding = program.command("funding").description("Funding rate comparison across exchanges");
8
9
  // ── perp funding rates ── (default: show top symbols)
9
10
  funding
@@ -38,8 +39,15 @@ export function registerFundingCommands(program, isJson) {
38
39
  saveFundingSnapshot(allRates);
39
40
  }
40
41
  catch { /* non-critical */ }
41
- if (isJson())
42
+ if (isJson()) {
43
+ // When --ndjson or --fields is used, return the symbols array directly
44
+ // so each symbol gets its own line / fields can be picked per element.
45
+ // Otherwise return the full snapshot (includes exchangeStatus metadata).
46
+ if (process.argv.includes("--ndjson") || process.argv.includes("--fields")) {
47
+ return printJson(jsonOk(snapshot.symbols));
48
+ }
42
49
  return printJson(jsonOk(snapshot));
50
+ }
43
51
  printSnapshotTable(snapshot);
44
52
  printExchangeStatus(snapshot);
45
53
  console.log(chalk.gray(" * Rates shown are current predictions. Actual settled rates may differ.\n"));
@@ -234,6 +242,205 @@ export function registerFundingCommands(program, isJson) {
234
242
  await new Promise(r => setTimeout(r, intervalSec * 1000));
235
243
  }
236
244
  });
245
+ // ── perp funding positions ── (funding impact on open positions)
246
+ funding
247
+ .command("positions")
248
+ .description("Show funding rate impact on your current positions")
249
+ .option("--exchanges <list>", "Comma-separated exchanges to check (default: all with keys)")
250
+ .action(async (opts) => {
251
+ if (!getAdapterForExchange) {
252
+ const msg = "funding positions requires adapter access";
253
+ if (isJson())
254
+ return printJson(jsonError("INTERNAL", msg));
255
+ console.log(chalk.red(` ${msg}\n`));
256
+ return;
257
+ }
258
+ await withJsonErrors(isJson(), async () => {
259
+ const targetExchanges = opts.exchanges
260
+ ? opts.exchanges.split(",").map(s => s.trim().toLowerCase())
261
+ : [...ALL_EXCHANGES];
262
+ if (!isJson())
263
+ console.log(chalk.cyan(" Fetching positions and funding rates...\n"));
264
+ const results = [];
265
+ const exchangeErrors = {};
266
+ await Promise.all(targetExchanges.map(async (exchange) => {
267
+ try {
268
+ const adapter = await getAdapterForExchange(exchange);
269
+ const [positions, markets, fundingPayments] = await Promise.all([
270
+ adapter.getPositions(),
271
+ adapter.getMarkets(),
272
+ adapter.getFundingPayments(100).catch(() => []),
273
+ ]);
274
+ if (positions.length === 0)
275
+ return;
276
+ // Build symbol → funding rate map
277
+ const fundingMap = new Map();
278
+ for (const m of markets) {
279
+ const sym = m.symbol.toUpperCase().replace(/-PERP$/, "");
280
+ fundingMap.set(sym, {
281
+ rate: Number(m.fundingRate) || 0,
282
+ markPrice: Number(m.markPrice) || 0,
283
+ });
284
+ }
285
+ // Aggregate actual funding payments per symbol (last 24h)
286
+ const now = Date.now();
287
+ const dayAgo = now - 24 * 60 * 60 * 1000;
288
+ const actualFunding = new Map();
289
+ for (const fp of fundingPayments) {
290
+ if (fp.time < dayAgo)
291
+ continue;
292
+ const sym = fp.symbol.toUpperCase().replace(/-PERP$/, "");
293
+ const amt = Number(fp.payment) || 0;
294
+ if (!actualFunding.has(sym))
295
+ actualFunding.set(sym, { received: 0, paid: 0 });
296
+ const entry = actualFunding.get(sym);
297
+ if (amt > 0)
298
+ entry.received += amt;
299
+ else
300
+ entry.paid += Math.abs(amt);
301
+ }
302
+ for (const pos of positions) {
303
+ const sym = pos.symbol.toUpperCase().replace(/-PERP$/, "");
304
+ const fdata = fundingMap.get(sym);
305
+ const fundingRate = fdata?.rate ?? 0;
306
+ const mark = Number(pos.markPrice) || fdata?.markPrice || 0;
307
+ const size = Math.abs(Number(pos.size));
308
+ const notionalUsd = size * mark;
309
+ const hourlyRate = toHourlyRate(fundingRate, exchange);
310
+ const annualPct = annualizeRate(fundingRate, exchange);
311
+ const hourlyPayment = estimateHourlyFunding(fundingRate, exchange, notionalUsd, pos.side);
312
+ const dailyPayment = hourlyPayment * 24;
313
+ const actual = actualFunding.get(sym);
314
+ const actualReceived24h = actual?.received ?? 0;
315
+ const actualPaid24h = actual?.paid ?? 0;
316
+ const actualNet24h = actualReceived24h - actualPaid24h;
317
+ results.push({
318
+ exchange,
319
+ symbol: sym,
320
+ side: pos.side,
321
+ size: pos.size,
322
+ entryPrice: pos.entryPrice,
323
+ markPrice: pos.markPrice,
324
+ unrealizedPnl: pos.unrealizedPnl,
325
+ leverage: pos.leverage,
326
+ notionalUsd,
327
+ fundingRate,
328
+ hourlyRate,
329
+ annualPct,
330
+ hourlyPayment,
331
+ dailyPayment,
332
+ actualReceived24h,
333
+ actualPaid24h,
334
+ actualNet24h,
335
+ });
336
+ }
337
+ }
338
+ catch (err) {
339
+ exchangeErrors[exchange] = err instanceof Error ? err.message : String(err);
340
+ }
341
+ }));
342
+ if (results.length === 0) {
343
+ if (isJson()) {
344
+ return printJson(jsonOk({
345
+ positions: [],
346
+ totals: {
347
+ predicted: { hourly: 0, daily: 0 },
348
+ actual24h: { net: 0 },
349
+ notionalUsd: 0,
350
+ },
351
+ errors: exchangeErrors,
352
+ }));
353
+ }
354
+ console.log(chalk.gray(" No open positions found on any exchange.\n"));
355
+ if (Object.keys(exchangeErrors).length > 0) {
356
+ for (const [ex, err] of Object.entries(exchangeErrors)) {
357
+ console.log(chalk.yellow(` ${ex}: ${err}`));
358
+ }
359
+ console.log();
360
+ }
361
+ return;
362
+ }
363
+ // Sort: biggest daily payment impact first
364
+ results.sort((a, b) => Math.abs(b.dailyPayment) - Math.abs(a.dailyPayment));
365
+ const totalHourly = results.reduce((s, r) => s + r.hourlyPayment, 0);
366
+ const totalDaily = results.reduce((s, r) => s + r.dailyPayment, 0);
367
+ const totalNotional = results.reduce((s, r) => s + r.notionalUsd, 0);
368
+ const totalActualNet = results.reduce((s, r) => s + r.actualNet24h, 0);
369
+ if (isJson()) {
370
+ return printJson(jsonOk({
371
+ positions: results.map(r => ({
372
+ exchange: r.exchange,
373
+ symbol: r.symbol,
374
+ side: r.side,
375
+ size: r.size,
376
+ notionalUsd: Number(r.notionalUsd.toFixed(2)),
377
+ fundingRate: r.fundingRate,
378
+ annualPct: Number(r.annualPct.toFixed(2)),
379
+ predicted: {
380
+ hourly: Number(r.hourlyPayment.toFixed(6)),
381
+ daily: Number(r.dailyPayment.toFixed(4)),
382
+ },
383
+ actual24h: {
384
+ received: Number(r.actualReceived24h.toFixed(6)),
385
+ paid: Number(r.actualPaid24h.toFixed(6)),
386
+ net: Number(r.actualNet24h.toFixed(6)),
387
+ },
388
+ unrealizedPnl: r.unrealizedPnl,
389
+ })),
390
+ totals: {
391
+ predicted: {
392
+ hourly: Number(totalHourly.toFixed(6)),
393
+ daily: Number(totalDaily.toFixed(4)),
394
+ },
395
+ actual24h: {
396
+ net: Number(totalActualNet.toFixed(6)),
397
+ },
398
+ notionalUsd: Number(totalNotional.toFixed(2)),
399
+ },
400
+ errors: Object.keys(exchangeErrors).length > 0 ? exchangeErrors : undefined,
401
+ }));
402
+ }
403
+ const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : e.toUpperCase();
404
+ console.log(chalk.cyan.bold(" Position Funding Impact\n"));
405
+ const rows = results.map(r => {
406
+ const sideColor = r.side === "long" ? chalk.green : chalk.red;
407
+ const rateColor = r.fundingRate > 0 ? chalk.red : r.fundingRate < 0 ? chalk.green : chalk.white;
408
+ // actual net: positive = received, negative = paid
409
+ const actualColor = r.actualNet24h > 0 ? chalk.green : r.actualNet24h < 0 ? chalk.red : chalk.gray;
410
+ const actualStr = r.actualNet24h !== 0
411
+ ? actualColor(`${r.actualNet24h > 0 ? "+" : ""}$${r.actualNet24h.toFixed(4)}`)
412
+ : chalk.gray("-");
413
+ return [
414
+ chalk.white.bold(exAbbr(r.exchange)),
415
+ chalk.white.bold(r.symbol),
416
+ sideColor(r.side.toUpperCase()),
417
+ `$${formatUsd(r.notionalUsd)}`,
418
+ rateColor(formatPercent(r.fundingRate)),
419
+ rateColor(`${r.annualPct.toFixed(1)}%`),
420
+ actualStr,
421
+ formatPnl(r.unrealizedPnl),
422
+ ];
423
+ });
424
+ console.log(makeTable(["Ex", "Symbol", "Side", "Notional", "Rate(now)", "Annual(now)", "Actual 24h", "uPnL"], rows));
425
+ // Summary
426
+ const actualNetColor = totalActualNet >= 0 ? chalk.green : chalk.red;
427
+ const predictColor = totalDaily >= 0 ? chalk.red : chalk.green;
428
+ const predictSign = totalDaily >= 0 ? "-" : "+";
429
+ console.log();
430
+ console.log(` Total Notional: $${formatUsd(totalNotional)}`);
431
+ console.log(` Actual Net (24h): ${actualNetColor(`${totalActualNet >= 0 ? "+" : ""}$${totalActualNet.toFixed(4)}`)}`);
432
+ console.log(` Predicted (now rate): ${predictColor(`${predictSign}$${Math.abs(totalDaily).toFixed(4)}/d`)}`);
433
+ if (Object.keys(exchangeErrors).length > 0) {
434
+ console.log();
435
+ for (const [ex, err] of Object.entries(exchangeErrors)) {
436
+ console.log(chalk.yellow(` ${exAbbr(ex)}: ${err}`));
437
+ }
438
+ }
439
+ console.log(chalk.gray("\n Actual 24h: real funding received/paid in the last 24 hours."));
440
+ console.log(chalk.gray(" Predicted: based on current rate only (changes every hour)."));
441
+ console.log(chalk.gray(" + = received, - = paid\n"));
442
+ });
443
+ });
237
444
  }
238
445
  // ── Display helpers ──
239
446
  function formatAvgRate(rate) {
@@ -0,0 +1,4 @@
1
+ import { Command } from "commander";
2
+ import type { ExchangeAdapter } from "../exchanges/interface.js";
3
+ import type { Network } from "../pacifica/index.js";
4
+ export declare function registerFundsCommands(program: Command, getAdapter: () => Promise<ExchangeAdapter>, isJson: () => boolean, getNetwork: () => Network): void;