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
@@ -1,2 +1,3 @@
1
1
  import { Command } from "commander";
2
- export declare function registerHistoryCommands(program: Command, isJson: () => boolean): void;
2
+ import type { ExchangeAdapter } from "../exchanges/interface.js";
3
+ export declare function registerHistoryCommands(program: Command, isJson: () => boolean, getAdapterForExchange?: (exchange: string) => Promise<ExchangeAdapter>): void;
@@ -1,8 +1,21 @@
1
1
  import chalk from "chalk";
2
- import { printJson, jsonOk, makeTable, formatUsd, formatPnl, withJsonErrors } from "../utils.js";
2
+ import { printJson, jsonOk, jsonError, makeTable, formatUsd, formatPnl, withJsonErrors } from "../utils.js";
3
3
  import { readExecutionLog, getExecutionStats, pruneExecutionLog } from "../execution-log.js";
4
4
  import { readPositionHistory, getPositionStats } from "../position-history.js";
5
- export function registerHistoryCommands(program, isJson) {
5
+ import { saveEquitySnapshot, readEquityHistory, computePnlMetrics, computeDailyPnl, aggregateWeekly, } from "../equity-tracker.js";
6
+ const EXCHANGES = ["pacifica", "hyperliquid", "lighter"];
7
+ function parseSince(since) {
8
+ if (!since)
9
+ return undefined;
10
+ const match = since.match(/^(\d+)(h|d|w)$/);
11
+ if (match) {
12
+ const [, num, unit] = match;
13
+ const ms = { h: 3600000, d: 86400000, w: 604800000 }[unit] ?? 86400000;
14
+ return new Date(Date.now() - parseInt(num) * ms).toISOString();
15
+ }
16
+ return since;
17
+ }
18
+ export function registerHistoryCommands(program, isJson, getAdapterForExchange) {
6
19
  const history = program.command("history").description("Execution log & audit trail");
7
20
  // ── history list ──
8
21
  history
@@ -66,10 +79,10 @@ export function registerHistoryCommands(program, isJson) {
66
79
  console.log(makeTable(["Time", "Exchange", "Type", "Symbol", "Side", "Size", "Notional", "Status", ""], rows));
67
80
  });
68
81
  });
69
- // ── history stats ──
70
- history
82
+ // ── history stats (hidden alias for 'history summary') ──
83
+ const statsCmd = history
71
84
  .command("stats")
72
- .description("Execution statistics summary")
85
+ .description("Use 'history summary'")
73
86
  .option("--since <date>", "Stats since date (ISO or relative: 24h, 7d, 30d)")
74
87
  .action(async (opts) => {
75
88
  await withJsonErrors(isJson(), async () => {
@@ -110,6 +123,7 @@ export function registerHistoryCommands(program, isJson) {
110
123
  console.log();
111
124
  });
112
125
  });
126
+ statsCmd._hidden = true;
113
127
  // ── history positions ──
114
128
  history
115
129
  .command("positions")
@@ -232,4 +246,493 @@ export function registerHistoryCommands(program, isJson) {
232
246
  }
233
247
  console.log(chalk.green(`\n Pruned ${pruned} old records (keeping last ${opts.keepDays} days).\n`));
234
248
  });
249
+ // ── Analytics subcommands (merged from analytics.ts) ──
250
+ if (getAdapterForExchange) {
251
+ registerAnalyticsSubcommands(history, getAdapterForExchange, isJson);
252
+ // Keep deprecated top-level 'analytics' alias (hidden from help)
253
+ const analytics = program.command("analytics").description("Use 'perp history'");
254
+ analytics._hidden = true;
255
+ registerAnalyticsSubcommands(analytics, getAdapterForExchange, isJson);
256
+ }
257
+ // ── PnL subcommands (merged from pnl.ts) ──
258
+ if (getAdapterForExchange) {
259
+ registerPnlSubcommands(history, getAdapterForExchange, isJson);
260
+ // Deprecated top-level pnl alias
261
+ const pnlAlias = program.command("pnl").description("Use 'perp history perf --period daily|weekly|summary'");
262
+ pnlAlias._hidden = true;
263
+ if (getAdapterForExchange)
264
+ registerPnlSubcommands(pnlAlias, getAdapterForExchange, isJson);
265
+ }
266
+ }
267
+ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
268
+ // ── analytics summary ──
269
+ parent
270
+ .command("summary")
271
+ .description("Execution log summary statistics")
272
+ .option("--since <period>", "Period: 24h, 7d, 30d, or ISO date")
273
+ .action(async (opts) => {
274
+ await withJsonErrors(isJson(), async () => {
275
+ const since = parseSince(opts.since);
276
+ const stats = getExecutionStats(since);
277
+ const records = readExecutionLog({ since });
278
+ // Calculate volume
279
+ let totalVolume = 0;
280
+ for (const r of records) {
281
+ if (r.notional)
282
+ totalVolume += r.notional;
283
+ else if (r.price && r.size)
284
+ totalVolume += Number(r.price) * Number(r.size);
285
+ }
286
+ const result = { ...stats, totalVolume, period: opts.since ?? "all time" };
287
+ if (isJson())
288
+ return printJson(jsonOk(result));
289
+ console.log(chalk.cyan.bold(`\n Trading Summary ${opts.since ? `(${opts.since})` : "(all time)"}\n`));
290
+ console.log(` Total Trades: ${stats.totalTrades}`);
291
+ console.log(` Success Rate: ${stats.successRate.toFixed(1)}%`);
292
+ console.log(` Total Volume: $${formatUsd(totalVolume)}`);
293
+ if (Object.keys(stats.byExchange).length > 0) {
294
+ console.log(chalk.white.bold("\n By Exchange:"));
295
+ for (const [ex, count] of Object.entries(stats.byExchange)) {
296
+ console.log(` ${ex.padEnd(14)} ${count} trades`);
297
+ }
298
+ }
299
+ if (Object.keys(stats.byType).length > 0) {
300
+ console.log(chalk.white.bold("\n By Type:"));
301
+ for (const [type, count] of Object.entries(stats.byType)) {
302
+ console.log(` ${type.replace(/_/g, " ").padEnd(14)} ${count}`);
303
+ }
304
+ }
305
+ if (stats.recentErrors.length > 0) {
306
+ console.log(chalk.red.bold("\n Recent Errors:"));
307
+ for (const err of stats.recentErrors) {
308
+ console.log(` ${chalk.red(err)}`);
309
+ }
310
+ }
311
+ console.log();
312
+ });
313
+ });
314
+ // ── analytics pnl ──
315
+ parent
316
+ .command("pnl")
317
+ .description("Realized P&L from exchange trade history (volume & fees by symbol/exchange)")
318
+ .option("-e, --exchange <exchanges>", "Comma-separated exchanges")
319
+ .option("--since <period>", "Period: 24h, 7d, 30d")
320
+ .option("-n, --limit <n>", "Trade history limit per exchange", "100")
321
+ .action(async (opts) => {
322
+ await withJsonErrors(isJson(), async () => {
323
+ const exchanges = opts.exchange
324
+ ? opts.exchange.split(",").map(e => e.trim())
325
+ : [...EXCHANGES];
326
+ const limit = parseInt(opts.limit);
327
+ const sinceMs = opts.since ? new Date(parseSince(opts.since)).getTime() : 0;
328
+ const allTrades = [];
329
+ await Promise.allSettled(exchanges.map(async (ex) => {
330
+ try {
331
+ const adapter = await getAdapterForExchange(ex);
332
+ const trades = await adapter.getTradeHistory(limit);
333
+ for (const t of trades) {
334
+ if (sinceMs && t.time < sinceMs)
335
+ continue;
336
+ allTrades.push({
337
+ exchange: ex,
338
+ symbol: t.symbol,
339
+ side: t.side,
340
+ price: Number(t.price),
341
+ size: Number(t.size),
342
+ fee: Number(t.fee),
343
+ time: t.time,
344
+ });
345
+ }
346
+ }
347
+ catch { /* skip unavailable exchanges */ }
348
+ }));
349
+ // Aggregate by symbol
350
+ const bySymbol = new Map();
351
+ const byExchange = new Map();
352
+ let totalVolume = 0;
353
+ let totalFees = 0;
354
+ for (const t of allTrades) {
355
+ const notional = t.price * t.size;
356
+ totalVolume += notional;
357
+ totalFees += t.fee;
358
+ const sym = bySymbol.get(t.symbol) ?? { volume: 0, fees: 0, trades: 0 };
359
+ sym.volume += notional;
360
+ sym.fees += t.fee;
361
+ sym.trades++;
362
+ bySymbol.set(t.symbol, sym);
363
+ const ex = byExchange.get(t.exchange) ?? { volume: 0, fees: 0, trades: 0 };
364
+ ex.volume += notional;
365
+ ex.fees += t.fee;
366
+ ex.trades++;
367
+ byExchange.set(t.exchange, ex);
368
+ }
369
+ const result = {
370
+ totalTrades: allTrades.length,
371
+ totalVolume,
372
+ totalFees,
373
+ netAfterFees: -totalFees, // realized PnL would need entry/exit matching; fees are definite cost
374
+ bySymbol: Object.fromEntries(bySymbol),
375
+ byExchange: Object.fromEntries(byExchange),
376
+ };
377
+ if (isJson())
378
+ return printJson(jsonOk(result));
379
+ console.log(chalk.cyan.bold(`\n Realized Trading P&L ${opts.since ? `(${opts.since})` : ""}\n`));
380
+ console.log(` Total Trades: ${allTrades.length}`);
381
+ console.log(` Total Volume: $${formatUsd(totalVolume)}`);
382
+ console.log(` Total Fees: ${chalk.red(`-$${formatUsd(totalFees)}`)}`);
383
+ if (bySymbol.size > 0) {
384
+ console.log(chalk.white.bold("\n By Symbol:"));
385
+ const symRows = [...bySymbol.entries()]
386
+ .sort((a, b) => b[1].volume - a[1].volume)
387
+ .map(([sym, d]) => [
388
+ chalk.white.bold(sym),
389
+ String(d.trades),
390
+ `$${formatUsd(d.volume)}`,
391
+ chalk.red(`-$${formatUsd(d.fees)}`),
392
+ ]);
393
+ console.log(makeTable(["Symbol", "Trades", "Volume", "Fees"], symRows));
394
+ }
395
+ if (byExchange.size > 0) {
396
+ console.log(chalk.white.bold(" By Exchange:"));
397
+ const exRows = [...byExchange.entries()].map(([ex, d]) => [
398
+ chalk.white.bold(ex),
399
+ String(d.trades),
400
+ `$${formatUsd(d.volume)}`,
401
+ chalk.red(`-$${formatUsd(d.fees)}`),
402
+ ]);
403
+ console.log(makeTable(["Exchange", "Trades", "Volume", "Fees"], exRows));
404
+ }
405
+ });
406
+ });
407
+ // ── analytics funding ──
408
+ parent
409
+ .command("funding")
410
+ .description("Funding payment aggregation across exchanges (net by symbol/exchange)")
411
+ .option("-e, --exchange <exchanges>", "Comma-separated exchanges")
412
+ .option("-s, --symbol <symbol>", "Filter by symbol")
413
+ .option("-n, --limit <n>", "Funding history limit per exchange", "50")
414
+ .action(async (opts) => {
415
+ await withJsonErrors(isJson(), async () => {
416
+ const exchanges = opts.exchange
417
+ ? opts.exchange.split(",").map(e => e.trim())
418
+ : [...EXCHANGES];
419
+ const allFunding = [];
420
+ await Promise.allSettled(exchanges.map(async (ex) => {
421
+ try {
422
+ const adapter = await getAdapterForExchange(ex);
423
+ const payments = await adapter.getFundingPayments(parseInt(opts.limit));
424
+ for (const p of payments) {
425
+ if (opts.symbol && !p.symbol.toUpperCase().includes(opts.symbol.toUpperCase()))
426
+ continue;
427
+ allFunding.push({
428
+ exchange: ex,
429
+ symbol: p.symbol,
430
+ payment: Number(p.payment),
431
+ time: p.time,
432
+ });
433
+ }
434
+ }
435
+ catch { /* skip */ }
436
+ }));
437
+ // Aggregate
438
+ const bySymbol = new Map();
439
+ const byExchange = new Map();
440
+ let totalFunding = 0;
441
+ for (const f of allFunding) {
442
+ totalFunding += f.payment;
443
+ bySymbol.set(f.symbol, (bySymbol.get(f.symbol) ?? 0) + f.payment);
444
+ byExchange.set(f.exchange, (byExchange.get(f.exchange) ?? 0) + f.payment);
445
+ }
446
+ const result = {
447
+ totalPayments: allFunding.length,
448
+ totalFunding,
449
+ bySymbol: Object.fromEntries(bySymbol),
450
+ byExchange: Object.fromEntries(byExchange),
451
+ };
452
+ if (isJson())
453
+ return printJson(jsonOk(result));
454
+ console.log(chalk.cyan.bold("\n Funding Payment Summary\n"));
455
+ console.log(` Total Payments: ${allFunding.length}`);
456
+ console.log(` Net Funding: ${formatPnl(totalFunding)}`);
457
+ if (bySymbol.size > 0) {
458
+ console.log(chalk.white.bold("\n By Symbol:"));
459
+ const rows = [...bySymbol.entries()]
460
+ .sort((a, b) => b[1] - a[1])
461
+ .map(([sym, amt]) => [chalk.white.bold(sym), formatPnl(amt)]);
462
+ console.log(makeTable(["Symbol", "Net Funding"], rows));
463
+ }
464
+ if (byExchange.size > 0) {
465
+ console.log(chalk.white.bold(" By Exchange:"));
466
+ const rows = [...byExchange.entries()]
467
+ .map(([ex, amt]) => [chalk.white.bold(ex), formatPnl(amt)]);
468
+ console.log(makeTable(["Exchange", "Net Funding"], rows));
469
+ }
470
+ });
471
+ });
472
+ // ── analytics report ──
473
+ parent
474
+ .command("report")
475
+ .description("Full performance report (summary + pnl + funding)")
476
+ .option("-e, --exchange <exchanges>", "Comma-separated exchanges")
477
+ .option("--since <period>", "Period: 24h, 7d, 30d")
478
+ .action(async (opts) => {
479
+ await withJsonErrors(isJson(), async () => {
480
+ const since = parseSince(opts.since);
481
+ const stats = getExecutionStats(since);
482
+ const records = readExecutionLog({ since });
483
+ const exchanges = opts.exchange
484
+ ? opts.exchange.split(",").map(e => e.trim())
485
+ : [...EXCHANGES];
486
+ // Volume from log
487
+ let logVolume = 0;
488
+ for (const r of records) {
489
+ if (r.notional)
490
+ logVolume += r.notional;
491
+ }
492
+ // Fetch live data from exchanges
493
+ let totalFunding = 0;
494
+ let totalFees = 0;
495
+ let totalEquity = 0;
496
+ let totalUPnl = 0;
497
+ const symbolSet = new Set();
498
+ await Promise.allSettled(exchanges.map(async (ex) => {
499
+ try {
500
+ const adapter = await getAdapterForExchange(ex);
501
+ const [balance, trades, funding] = await Promise.all([
502
+ adapter.getBalance(),
503
+ adapter.getTradeHistory(50).catch(() => []),
504
+ adapter.getFundingPayments(50).catch(() => []),
505
+ ]);
506
+ totalEquity += Number(balance.equity);
507
+ totalUPnl += Number(balance.unrealizedPnl);
508
+ for (const t of trades) {
509
+ totalFees += Number(t.fee);
510
+ symbolSet.add(t.symbol);
511
+ }
512
+ for (const f of funding)
513
+ totalFunding += Number(f.payment);
514
+ }
515
+ catch { /* skip */ }
516
+ }));
517
+ const report = {
518
+ period: opts.since ?? "all time",
519
+ execution: {
520
+ totalTrades: stats.totalTrades,
521
+ successRate: stats.successRate,
522
+ volume: logVolume,
523
+ byExchange: stats.byExchange,
524
+ byType: stats.byType,
525
+ },
526
+ portfolio: {
527
+ totalEquity,
528
+ unrealizedPnl: totalUPnl,
529
+ },
530
+ costs: {
531
+ totalFees,
532
+ totalFunding,
533
+ netFunding: totalFunding,
534
+ },
535
+ insights: {
536
+ uniqueSymbols: symbolSet.size,
537
+ mostActiveExchange: Object.entries(stats.byExchange).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "none",
538
+ avgTradesPerDay: stats.totalTrades > 0
539
+ ? (stats.totalTrades / Math.max(1, (Date.now() - new Date(records[records.length - 1]?.timestamp ?? Date.now()).getTime()) / 86400000)).toFixed(1)
540
+ : "0",
541
+ },
542
+ };
543
+ if (isJson())
544
+ return printJson(jsonOk(report));
545
+ console.log(chalk.cyan.bold(`\n Performance Report ${opts.since ? `(${opts.since})` : ""}\n`));
546
+ console.log(chalk.white.bold(" Execution"));
547
+ console.log(` Trades: ${stats.totalTrades} (${stats.successRate.toFixed(0)}% success)`);
548
+ console.log(` Volume: $${formatUsd(logVolume)}`);
549
+ console.log(` Symbols Traded: ${symbolSet.size}`);
550
+ console.log(` Avg/Day: ${report.insights.avgTradesPerDay}`);
551
+ console.log(chalk.white.bold("\n Portfolio"));
552
+ console.log(` Total Equity: $${formatUsd(totalEquity)}`);
553
+ console.log(` Unrealized PnL: ${formatPnl(totalUPnl)}`);
554
+ console.log(chalk.white.bold("\n Costs & Income"));
555
+ console.log(` Trading Fees: ${chalk.red(`-$${formatUsd(totalFees)}`)}`);
556
+ console.log(` Funding Income: ${formatPnl(totalFunding)}`);
557
+ console.log(` Net: ${formatPnl(totalFunding - totalFees)}`);
558
+ console.log();
559
+ });
560
+ });
561
+ }
562
+ function parseSinceDate(since) {
563
+ if (!since)
564
+ return undefined;
565
+ const match = since.match(/^(\d+)(h|d|w)$/);
566
+ if (match) {
567
+ const [, num, unit] = match;
568
+ const ms = { h: 3600000, d: 86400000, w: 604800000 }[unit] ?? 86400000;
569
+ return new Date(Date.now() - parseInt(num) * ms);
570
+ }
571
+ return new Date(since);
572
+ }
573
+ /** Take equity snapshot for given adapters. */
574
+ async function takeSnapshot(getAdapterForExchange, exchanges) {
575
+ const ts = new Date().toISOString();
576
+ const snaps = [];
577
+ const results = await Promise.allSettled(exchanges.map(async (ex) => {
578
+ const adapter = await getAdapterForExchange(ex);
579
+ const [balance, positions] = await Promise.all([
580
+ adapter.getBalance(),
581
+ adapter.getPositions(),
582
+ ]);
583
+ const snap = {
584
+ ts,
585
+ exchange: ex,
586
+ equity: Number(balance.equity),
587
+ available: Number(balance.available),
588
+ marginUsed: Number(balance.marginUsed),
589
+ unrealizedPnl: Number(balance.unrealizedPnl),
590
+ positionCount: positions.filter(p => Number(p.size) > 0).length,
591
+ };
592
+ saveEquitySnapshot(snap);
593
+ return snap;
594
+ }));
595
+ for (const r of results) {
596
+ if (r.status === "fulfilled")
597
+ snaps.push(r.value);
598
+ }
599
+ return snaps;
600
+ }
601
+ function registerPnlSubcommands(parent, getAdapterForExchange, isJson) {
602
+ // ── snapshot ──
603
+ parent
604
+ .command("snapshot")
605
+ .description("Take equity snapshot across all exchanges")
606
+ .option("--exchanges <list>", "Comma-separated exchange list", EXCHANGES.join(","))
607
+ .action(async (opts) => {
608
+ await withJsonErrors(isJson(), async () => {
609
+ const exchanges = opts.exchanges.split(",").map(e => e.trim());
610
+ const snaps = await takeSnapshot(getAdapterForExchange, exchanges);
611
+ if (isJson()) {
612
+ printJson(jsonOk({ snapshots: snaps }));
613
+ }
614
+ else {
615
+ const total = snaps.reduce((s, sn) => s + sn.equity, 0);
616
+ console.log(chalk.cyan.bold("Equity Snapshot\n"));
617
+ for (const s of snaps) {
618
+ console.log(` ${s.exchange.padEnd(14)} $${formatUsd(s.equity)} ` +
619
+ `avail $${formatUsd(s.available)} ` +
620
+ `uPnL ${formatPnl(s.unrealizedPnl)} ` +
621
+ `pos: ${s.positionCount}`);
622
+ }
623
+ console.log(`\n ${"Total".padEnd(14)} $${formatUsd(total)}`);
624
+ console.log(chalk.gray(`\n Saved at ${snaps[0]?.ts ?? "—"}`));
625
+ }
626
+ });
627
+ });
628
+ // ── track ──
629
+ parent
630
+ .command("track")
631
+ .description("Continuously track equity (foreground)")
632
+ .option("--interval <sec>", "Snapshot interval in seconds", "300")
633
+ .option("--exchanges <list>", "Comma-separated exchange list", EXCHANGES.join(","))
634
+ .action(async (opts) => {
635
+ const intervalMs = parseInt(opts.interval) * 1000;
636
+ const exchanges = opts.exchanges.split(",").map(e => e.trim());
637
+ console.log(chalk.cyan(`PnL tracker started — snapshot every ${opts.interval}s for [${exchanges.join(", ")}]`));
638
+ console.log(chalk.gray("Press Ctrl+C to stop\n"));
639
+ const controller = new AbortController();
640
+ process.on("SIGINT", () => controller.abort());
641
+ // Initial snapshot
642
+ const snaps = await takeSnapshot(getAdapterForExchange, exchanges);
643
+ const total = snaps.reduce((s, sn) => s + sn.equity, 0);
644
+ console.log(`${chalk.gray(new Date().toLocaleTimeString())} Total equity: $${formatUsd(total)}`);
645
+ while (!controller.signal.aborted) {
646
+ await new Promise((resolve) => {
647
+ const timer = setTimeout(resolve, intervalMs);
648
+ controller.signal.addEventListener("abort", () => { clearTimeout(timer); resolve(); }, { once: true });
649
+ });
650
+ if (controller.signal.aborted)
651
+ break;
652
+ try {
653
+ const s = await takeSnapshot(getAdapterForExchange, exchanges);
654
+ const t = s.reduce((sum, sn) => sum + sn.equity, 0);
655
+ const delta = t - total;
656
+ console.log(`${chalk.gray(new Date().toLocaleTimeString())} ` +
657
+ `Total: $${formatUsd(t)} ${formatPnl(delta)}`);
658
+ }
659
+ catch (err) {
660
+ console.error(chalk.red(`Snapshot error: ${err instanceof Error ? err.message : err}`));
661
+ }
662
+ }
663
+ });
664
+ // ── perf (unified: daily/weekly/summary) ──
665
+ parent
666
+ .command("perf")
667
+ .description("Performance breakdown — daily, weekly, or summary stats")
668
+ .option("--period <period>", "daily, weekly, or summary", "daily")
669
+ .option("--exchange <name>", "Single exchange filter")
670
+ .option("--days <n>", "Number of days (daily mode)", "14")
671
+ .option("--weeks <n>", "Number of weeks (weekly mode)", "8")
672
+ .option("--since <period>", "Time range (e.g. 7d, 2w, 30d)")
673
+ .action(async (opts) => {
674
+ await withJsonErrors(isJson(), async () => {
675
+ const mode = opts.period.toLowerCase();
676
+ const defaultSince = mode === "weekly"
677
+ ? new Date(Date.now() - parseInt(opts.weeks) * 7 * 86400000)
678
+ : new Date(Date.now() - parseInt(opts.days) * 86400000);
679
+ const since = parseSinceDate(opts.since) ?? defaultSince;
680
+ const snapshots = readEquityHistory({ exchange: opts.exchange, since });
681
+ if (snapshots.length === 0) {
682
+ if (isJson()) {
683
+ printJson(jsonError("NO_DATA", "No equity snapshots found. Run 'perp history snapshot' or 'perp history track' first."));
684
+ }
685
+ else {
686
+ console.log(chalk.yellow("No equity snapshots found. Run 'perp history snapshot' or 'perp history track' first."));
687
+ }
688
+ return;
689
+ }
690
+ if (mode === "summary") {
691
+ const metrics = computePnlMetrics(snapshots, opts.exchange);
692
+ if (isJson()) {
693
+ printJson(jsonOk({ ...metrics, dailyReturns: undefined, dailyCount: metrics.dailyReturns.length }));
694
+ }
695
+ else {
696
+ console.log(chalk.cyan.bold(`Performance Summary${opts.exchange ? ` (${opts.exchange})` : ""}\n`));
697
+ console.log(` Period: ${metrics.period.from} → ${metrics.period.to} (${metrics.period.days} days)`);
698
+ console.log(` Total Return: ${formatPnl(metrics.totalReturn)} (${metrics.totalReturnPct >= 0 ? "+" : ""}${metrics.totalReturnPct.toFixed(2)}%)`);
699
+ console.log(` Peak Equity: $${formatUsd(metrics.peakEquity)}`);
700
+ console.log(` Avg Daily PnL: ${formatPnl(metrics.avgDailyPnl)}`);
701
+ console.log();
702
+ console.log(` Sharpe Ratio: ${metrics.sharpeRatio >= 0 ? chalk.green(metrics.sharpeRatio.toFixed(2)) : chalk.red(metrics.sharpeRatio.toFixed(2))}`);
703
+ console.log(` Max Drawdown: ${chalk.red(`$${formatUsd(metrics.maxDrawdown)}`)} (${chalk.red(`-${metrics.maxDrawdownPct.toFixed(2)}%`)})`);
704
+ console.log(` Current DD: $${formatUsd(metrics.currentDrawdown)} (-${metrics.currentDrawdownPct.toFixed(2)}%)`);
705
+ console.log();
706
+ console.log(` Win Days: ${chalk.green(String(metrics.winDays))} / ${metrics.winDays + metrics.lossDays}`);
707
+ console.log(` Win Rate: ${metrics.winRate.toFixed(1)}%`);
708
+ if (metrics.bestDay)
709
+ console.log(` Best Day: ${metrics.bestDay.date} ${formatPnl(metrics.bestDay.pnl)}`);
710
+ if (metrics.worstDay)
711
+ console.log(` Worst Day: ${metrics.worstDay.date} ${formatPnl(metrics.worstDay.pnl)}`);
712
+ }
713
+ return;
714
+ }
715
+ const daily = computeDailyPnl(snapshots, opts.exchange);
716
+ if (mode === "weekly") {
717
+ const weekly = aggregateWeekly(daily);
718
+ if (isJson()) {
719
+ printJson(jsonOk({ weekly }));
720
+ return;
721
+ }
722
+ console.log(chalk.cyan.bold(`Weekly PnL${opts.exchange ? ` (${opts.exchange})` : ""}\n`));
723
+ const rows = weekly.map(w => [w.date, `$${formatUsd(w.startEquity)}`, `$${formatUsd(w.endEquity)}`, formatPnl(w.pnl), `${w.pnlPct >= 0 ? "+" : ""}${w.pnlPct.toFixed(2)}%`]);
724
+ console.log(makeTable(["Week", "Start", "End", "PnL", "PnL %"], rows));
725
+ }
726
+ else {
727
+ // daily (default)
728
+ if (isJson()) {
729
+ printJson(jsonOk({ daily }));
730
+ return;
731
+ }
732
+ console.log(chalk.cyan.bold(`Daily PnL${opts.exchange ? ` (${opts.exchange})` : ""}\n`));
733
+ const rows = daily.map(d => [d.date, `$${formatUsd(d.startEquity)}`, `$${formatUsd(d.endEquity)}`, formatPnl(d.pnl), `${d.pnlPct >= 0 ? "+" : ""}${d.pnlPct.toFixed(2)}%`]);
734
+ console.log(makeTable(["Date", "Start", "End", "PnL", "PnL %"], rows));
735
+ }
736
+ });
737
+ });
235
738
  }
@@ -3,7 +3,7 @@ import { printJson, jsonOk } from "../utils.js";
3
3
  import { setEnvVar } from "./init.js";
4
4
  import chalk from "chalk";
5
5
  export function registerManageCommands(program, getAdapter, isJson, getPacificaAdapter) {
6
- const manage = program.command("manage").description("Account management");
6
+ const manage = program.command("manage").description("Exchange account settings (margin, subaccount, API keys, builder)");
7
7
  // Ensure adapter is initialized before accessing PacificaAdapter
8
8
  async function pac() {
9
9
  const adapter = await getAdapter();
@@ -27,16 +27,18 @@ export function registerManageCommands(program, getAdapter, isJson, getPacificaA
27
27
  return printJson(jsonOk(result));
28
28
  console.log(chalk.green(`\n Margin mode for ${symbol.toUpperCase()} set to ${m}.\n`));
29
29
  });
30
- manage
30
+ const withdrawCmd = manage
31
31
  .command("withdraw <amount> <address>")
32
- .description("Withdraw funds to a Solana address")
32
+ .description("Use 'perp withdraw pacifica <amount>'")
33
33
  .action(async (amount, address) => {
34
+ console.log(chalk.yellow("\n Use 'perp withdraw pacifica <amount>' instead.\n"));
34
35
  const a = await pac();
35
36
  const result = await a.sdk.withdraw({ amount, dest_address: address }, a.publicKey, a.signer);
36
37
  if (isJson())
37
38
  return printJson(jsonOk(result));
38
39
  console.log(chalk.green(`\n Withdrawal of $${amount} to ${address} submitted.\n`));
39
40
  });
41
+ withdrawCmd._hidden = true;
40
42
  // Subaccounts
41
43
  const sub = manage.command("sub").description("Subaccount management");
42
44
  sub
@@ -274,7 +276,7 @@ export function registerManageCommands(program, getAdapter, isJson, getPacificaA
274
276
  manage
275
277
  .command("setup-api-key")
276
278
  .description("Generate & register a Lighter API key (required for trading)")
277
- .option("--key-index <n>", "API key index (2-254, default: 2)", "2")
279
+ .option("--key-index <n>", "API key index (4-254, default: 4)", "4")
278
280
  .action(async (opts) => {
279
281
  const adapter = await getAdapter();
280
282
  const { LighterAdapter } = await import("../exchanges/lighter.js");
@@ -1,5 +1,6 @@
1
1
  import { PacificaAdapter } from "../exchanges/pacifica.js";
2
- import { makeTable, formatUsd, formatPercent, printJson, jsonOk, jsonError } from "../utils.js";
2
+ import { HyperliquidAdapter } from "../exchanges/hyperliquid.js";
3
+ import { makeTable, formatUsd, formatPercent, printJson, jsonOk, jsonError, withJsonErrors } from "../utils.js";
3
4
  import chalk from "chalk";
4
5
  export function registerMarketCommands(program, getAdapter, isJson) {
5
6
  const market = program.command("market").description("Market data commands");
@@ -139,7 +140,7 @@ export function registerMarketCommands(program, getAdapter, isJson) {
139
140
  const rows = history.map((h) => [
140
141
  new Date(h.time).toLocaleString(),
141
142
  formatPercent(h.rate),
142
- `$${formatUsd(h.price)}`,
143
+ h.price != null ? `$${formatUsd(h.price)}` : chalk.gray("n/a"),
143
144
  ]);
144
145
  console.log(makeTable(["Time", "Funding Rate", "Oracle"], rows));
145
146
  });
@@ -222,4 +223,36 @@ export function registerMarketCommands(program, getAdapter, isJson) {
222
223
  ]);
223
224
  console.log(makeTable(["Time", "Open", "High", "Low", "Close", "Volume", "Trades"], rows));
224
225
  });
226
+ // ── market hip3 ── (HIP-3 deployed perp dexes on Hyperliquid)
227
+ market
228
+ .command("hip3")
229
+ .description("List HIP-3 deployed perp dexes (Hyperliquid only)")
230
+ .action(async () => {
231
+ await withJsonErrors(isJson(), async () => {
232
+ const adapter = await getAdapter();
233
+ if (!(adapter instanceof HyperliquidAdapter)) {
234
+ if (isJson())
235
+ return printJson(jsonError("INVALID_EXCHANGE", "HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid."));
236
+ console.error(chalk.red("\n HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid.\n"));
237
+ return;
238
+ }
239
+ const dexes = await adapter.listDeployedDexes();
240
+ if (isJson())
241
+ return printJson(jsonOk(dexes));
242
+ if (dexes.length === 0) {
243
+ console.log(chalk.gray("\n No deployed dexes found.\n"));
244
+ return;
245
+ }
246
+ console.log(chalk.cyan.bold("\n HIP-3 Deployed Perp DEXes\n"));
247
+ const rows = dexes.map(d => [
248
+ chalk.white.bold(d.name),
249
+ chalk.gray(d.deployer.slice(0, 10) + "..."),
250
+ String(d.assets.length),
251
+ d.assets.slice(0, 5).join(", ") + (d.assets.length > 5 ? ` +${d.assets.length - 5}` : ""),
252
+ ]);
253
+ console.log(makeTable(["DEX", "Deployer", "Assets", "Markets"], rows));
254
+ console.log(chalk.gray(`\n Use --dex <name> to trade on a deployed dex.`));
255
+ console.log(chalk.gray(` Example: perp -e hl --dex xyz market list\n`));
256
+ });
257
+ });
225
258
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Multi-leg order command.
3
+ *
4
+ * Executes multiple orders across exchanges simultaneously using Promise.allSettled.
5
+ * Supports automatic rollback if one leg fails.
6
+ *
7
+ * Usage:
8
+ * perp multi "hl:ETH:buy:0.01" "pac:ETH:sell:0.01"
9
+ * perp multi "hl:BTC:buy:0.001" "lighter:BTC:sell:0.001" --smart
10
+ * perp multi "hl:ETH:buy:0.01" "pac:SOL:buy:1" "lighter:BTC:sell:0.0001"
11
+ *
12
+ * Leg format: exchange:symbol:side:size
13
+ */
14
+ import { Command } from "commander";
15
+ import type { ExchangeAdapter } from "../exchanges/interface.js";
16
+ export declare function registerMultilegCommands(program: Command, getAdapterForExchange: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean): void;