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
@@ -0,0 +1,174 @@
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 chalk from "chalk";
15
+ import { printJson, jsonOk, jsonError, withJsonErrors } from "../utils.js";
16
+ import { logExecution } from "../execution-log.js";
17
+ import { smartOrder } from "../smart-order.js";
18
+ function parseLeg(spec) {
19
+ const parts = spec.split(":");
20
+ if (parts.length !== 4) {
21
+ throw new Error(`Invalid leg format "${spec}" — expected exchange:symbol:side:size`);
22
+ }
23
+ const [exchange, symbol, side, size] = parts;
24
+ // Normalize exchange aliases
25
+ const exMap = {
26
+ hl: "hyperliquid", pac: "pacifica", lit: "lighter",
27
+ hyperliquid: "hyperliquid", pacifica: "pacifica", lighter: "lighter",
28
+ };
29
+ const normalizedExchange = exMap[exchange.toLowerCase()];
30
+ if (!normalizedExchange) {
31
+ throw new Error(`Unknown exchange "${exchange}" in leg spec`);
32
+ }
33
+ if (side !== "buy" && side !== "sell") {
34
+ throw new Error(`Invalid side "${side}" — must be "buy" or "sell"`);
35
+ }
36
+ if (isNaN(Number(size)) || Number(size) <= 0) {
37
+ throw new Error(`Invalid size "${size}" — must be a positive number`);
38
+ }
39
+ return { exchange: normalizedExchange, symbol: symbol.toUpperCase(), side, size };
40
+ }
41
+ export function registerMultilegCommands(program, getAdapterForExchange, isJson) {
42
+ program
43
+ .command("multi <legs...>")
44
+ .description("Execute multi-leg orders simultaneously (exchange:symbol:side:size)")
45
+ .option("--smart", "Use smart order (IOC limit + fallback) for each leg")
46
+ .option("--rollback", "Auto-rollback filled legs if any leg fails", true)
47
+ .option("--no-rollback", "Disable auto-rollback")
48
+ .option("--timeout <ms>", "Per-leg timeout in milliseconds", "30000")
49
+ .action(async (legSpecs, opts) => {
50
+ await withJsonErrors(isJson(), async () => {
51
+ // Parse legs
52
+ const legs = legSpecs.map(parseLeg);
53
+ if (legs.length < 2) {
54
+ const err = "Multi-leg requires at least 2 legs";
55
+ if (isJson()) {
56
+ printJson(jsonError("INVALID_ARGS", err));
57
+ return;
58
+ }
59
+ throw new Error(err);
60
+ }
61
+ if (!isJson()) {
62
+ console.log(chalk.cyan.bold("Multi-Leg Order\n"));
63
+ for (const l of legs) {
64
+ const color = l.side === "buy" ? chalk.green : chalk.red;
65
+ console.log(` ${l.exchange.padEnd(14)} ${color(l.side.toUpperCase())} ${l.symbol} x ${l.size}`);
66
+ }
67
+ console.log();
68
+ }
69
+ // Get adapters (deduplicated)
70
+ const adapters = new Map();
71
+ for (const l of legs) {
72
+ if (!adapters.has(l.exchange)) {
73
+ adapters.set(l.exchange, await getAdapterForExchange(l.exchange));
74
+ }
75
+ }
76
+ // Execute all legs simultaneously
77
+ const timeoutMs = parseInt(opts.timeout);
78
+ const results = await Promise.allSettled(legs.map(async (leg) => {
79
+ const adapter = adapters.get(leg.exchange);
80
+ const orderPromise = opts.smart
81
+ ? smartOrder(adapter, leg.symbol, leg.side, leg.size).then(r => r.result)
82
+ : adapter.marketOrder(leg.symbol, leg.side, leg.size);
83
+ // Apply timeout
84
+ const result = await Promise.race([
85
+ orderPromise,
86
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Leg timeout")), timeoutMs)),
87
+ ]);
88
+ // Log execution
89
+ logExecution({
90
+ type: "multi_leg",
91
+ exchange: leg.exchange,
92
+ symbol: leg.symbol,
93
+ side: leg.side,
94
+ size: leg.size,
95
+ status: "success",
96
+ dryRun: false,
97
+ meta: { result, smart: opts.smart },
98
+ });
99
+ return { leg, status: "filled", result };
100
+ }));
101
+ // Process results
102
+ const legResults = results.map((r, i) => {
103
+ if (r.status === "fulfilled")
104
+ return r.value;
105
+ return {
106
+ leg: legs[i],
107
+ status: "failed",
108
+ error: r.reason instanceof Error ? r.reason.message : String(r.reason),
109
+ };
110
+ });
111
+ const filled = legResults.filter(r => r.status === "filled");
112
+ const failed = legResults.filter(r => r.status === "failed");
113
+ // Rollback if partial fill and rollback enabled
114
+ if (failed.length > 0 && filled.length > 0 && opts.rollback) {
115
+ if (!isJson()) {
116
+ console.log(chalk.yellow(`\n ${failed.length} leg(s) failed — rolling back ${filled.length} filled leg(s)...\n`));
117
+ }
118
+ for (const fr of filled) {
119
+ const adapter = adapters.get(fr.leg.exchange);
120
+ const reverseSide = fr.leg.side === "buy" ? "sell" : "buy";
121
+ try {
122
+ await adapter.marketOrder(fr.leg.symbol, reverseSide, fr.leg.size);
123
+ fr.status = "rolled_back";
124
+ logExecution({
125
+ type: "multi_leg_rollback",
126
+ exchange: fr.leg.exchange,
127
+ symbol: fr.leg.symbol,
128
+ side: reverseSide,
129
+ size: fr.leg.size,
130
+ status: "success",
131
+ dryRun: false,
132
+ });
133
+ if (!isJson()) {
134
+ console.log(chalk.gray(` Rolled back: ${fr.leg.exchange} ${reverseSide} ${fr.leg.symbol} x ${fr.leg.size}`));
135
+ }
136
+ }
137
+ catch (err) {
138
+ if (!isJson()) {
139
+ console.log(chalk.red(` Rollback failed: ${fr.leg.exchange} ${fr.leg.symbol} — ${err instanceof Error ? err.message : err}`));
140
+ }
141
+ }
142
+ }
143
+ }
144
+ if (isJson()) {
145
+ printJson(jsonOk({
146
+ legs: legResults.map(r => ({
147
+ exchange: r.leg.exchange,
148
+ symbol: r.leg.symbol,
149
+ side: r.leg.side,
150
+ size: r.leg.size,
151
+ status: r.status,
152
+ error: r.error,
153
+ })),
154
+ summary: {
155
+ total: legs.length,
156
+ filled: legResults.filter(r => r.status === "filled").length,
157
+ failed: failed.length,
158
+ rolledBack: legResults.filter(r => r.status === "rolled_back").length,
159
+ },
160
+ }));
161
+ }
162
+ else {
163
+ console.log(chalk.cyan.bold("\nResults:\n"));
164
+ for (const r of legResults) {
165
+ const icon = r.status === "filled" ? chalk.green("OK") :
166
+ r.status === "rolled_back" ? chalk.yellow("ROLLBACK") :
167
+ chalk.red("FAIL");
168
+ console.log(` ${icon} ${r.leg.exchange.padEnd(14)} ${r.leg.side} ${r.leg.symbol} x ${r.leg.size}${r.error ? ` — ${r.error}` : ""}`);
169
+ }
170
+ console.log(`\n Total: ${legs.length} Filled: ${filled.length} Failed: ${failed.length} Rolled back: ${legResults.filter(r => r.status === "rolled_back").length}`);
171
+ }
172
+ });
173
+ });
174
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * PnL Tracker CLI commands.
3
+ *
4
+ * perp pnl snapshot — take one-time equity snapshots across exchanges
5
+ * perp pnl track — continuous equity tracking (foreground)
6
+ * perp pnl daily — daily PnL breakdown table
7
+ * perp pnl weekly — weekly PnL breakdown table
8
+ * perp pnl summary — performance summary (Sharpe, drawdown, win rate)
9
+ */
10
+ import { Command } from "commander";
11
+ import type { ExchangeAdapter } from "../exchanges/interface.js";
12
+ export declare function registerPnlCommands(program: Command, getAdapterForExchange: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean): void;
@@ -0,0 +1,243 @@
1
+ /**
2
+ * PnL Tracker CLI commands.
3
+ *
4
+ * perp pnl snapshot — take one-time equity snapshots across exchanges
5
+ * perp pnl track — continuous equity tracking (foreground)
6
+ * perp pnl daily — daily PnL breakdown table
7
+ * perp pnl weekly — weekly PnL breakdown table
8
+ * perp pnl summary — performance summary (Sharpe, drawdown, win rate)
9
+ */
10
+ import chalk from "chalk";
11
+ import { printJson, jsonOk, jsonError, makeTable, formatUsd, formatPnl, withJsonErrors } from "../utils.js";
12
+ import { saveEquitySnapshot, readEquityHistory, computePnlMetrics, computeDailyPnl, aggregateWeekly, } from "../equity-tracker.js";
13
+ const EXCHANGES = ["pacifica", "hyperliquid", "lighter"];
14
+ function parseSince(since) {
15
+ if (!since)
16
+ return undefined;
17
+ const match = since.match(/^(\d+)(h|d|w)$/);
18
+ if (match) {
19
+ const [, num, unit] = match;
20
+ const ms = { h: 3600000, d: 86400000, w: 604800000 }[unit] ?? 86400000;
21
+ return new Date(Date.now() - parseInt(num) * ms);
22
+ }
23
+ return new Date(since);
24
+ }
25
+ /** Take equity snapshot for given adapters. */
26
+ async function takeSnapshot(getAdapterForExchange, exchanges) {
27
+ const ts = new Date().toISOString();
28
+ const snaps = [];
29
+ const results = await Promise.allSettled(exchanges.map(async (ex) => {
30
+ const adapter = await getAdapterForExchange(ex);
31
+ const [balance, positions] = await Promise.all([
32
+ adapter.getBalance(),
33
+ adapter.getPositions(),
34
+ ]);
35
+ const snap = {
36
+ ts,
37
+ exchange: ex,
38
+ equity: Number(balance.equity),
39
+ available: Number(balance.available),
40
+ marginUsed: Number(balance.marginUsed),
41
+ unrealizedPnl: Number(balance.unrealizedPnl),
42
+ positionCount: positions.filter(p => Number(p.size) > 0).length,
43
+ };
44
+ saveEquitySnapshot(snap);
45
+ return snap;
46
+ }));
47
+ for (const r of results) {
48
+ if (r.status === "fulfilled")
49
+ snaps.push(r.value);
50
+ }
51
+ return snaps;
52
+ }
53
+ export function registerPnlCommands(program, getAdapterForExchange, isJson) {
54
+ const pnl = program.command("pnl").description("Equity snapshot tracking & time-series performance (Sharpe, drawdown)");
55
+ // ── pnl snapshot ──
56
+ pnl
57
+ .command("snapshot")
58
+ .description("Take equity snapshot across all exchanges")
59
+ .option("--exchanges <list>", "Comma-separated exchange list", EXCHANGES.join(","))
60
+ .action(async (opts) => {
61
+ await withJsonErrors(isJson(), async () => {
62
+ const exchanges = opts.exchanges.split(",").map(e => e.trim());
63
+ const snaps = await takeSnapshot(getAdapterForExchange, exchanges);
64
+ if (isJson()) {
65
+ printJson(jsonOk({ snapshots: snaps }));
66
+ }
67
+ else {
68
+ const total = snaps.reduce((s, sn) => s + sn.equity, 0);
69
+ console.log(chalk.cyan.bold("Equity Snapshot\n"));
70
+ for (const s of snaps) {
71
+ console.log(` ${s.exchange.padEnd(14)} $${formatUsd(s.equity)} ` +
72
+ `avail $${formatUsd(s.available)} ` +
73
+ `uPnL ${formatPnl(s.unrealizedPnl)} ` +
74
+ `pos: ${s.positionCount}`);
75
+ }
76
+ console.log(`\n ${"Total".padEnd(14)} $${formatUsd(total)}`);
77
+ console.log(chalk.gray(`\n Saved at ${snaps[0]?.ts ?? "—"}`));
78
+ }
79
+ });
80
+ });
81
+ // ── pnl track ──
82
+ pnl
83
+ .command("track")
84
+ .description("Continuously track equity (foreground)")
85
+ .option("--interval <sec>", "Snapshot interval in seconds", "300")
86
+ .option("--exchanges <list>", "Comma-separated exchange list", EXCHANGES.join(","))
87
+ .action(async (opts) => {
88
+ const intervalMs = parseInt(opts.interval) * 1000;
89
+ const exchanges = opts.exchanges.split(",").map(e => e.trim());
90
+ console.log(chalk.cyan(`PnL tracker started — snapshot every ${opts.interval}s for [${exchanges.join(", ")}]`));
91
+ console.log(chalk.gray("Press Ctrl+C to stop\n"));
92
+ const controller = new AbortController();
93
+ process.on("SIGINT", () => controller.abort());
94
+ // Initial snapshot
95
+ const snaps = await takeSnapshot(getAdapterForExchange, exchanges);
96
+ const total = snaps.reduce((s, sn) => s + sn.equity, 0);
97
+ console.log(`${chalk.gray(new Date().toLocaleTimeString())} Total equity: $${formatUsd(total)}`);
98
+ while (!controller.signal.aborted) {
99
+ await new Promise((resolve) => {
100
+ const timer = setTimeout(resolve, intervalMs);
101
+ controller.signal.addEventListener("abort", () => { clearTimeout(timer); resolve(); }, { once: true });
102
+ });
103
+ if (controller.signal.aborted)
104
+ break;
105
+ try {
106
+ const s = await takeSnapshot(getAdapterForExchange, exchanges);
107
+ const t = s.reduce((sum, sn) => sum + sn.equity, 0);
108
+ const delta = t - total;
109
+ console.log(`${chalk.gray(new Date().toLocaleTimeString())} ` +
110
+ `Total: $${formatUsd(t)} ${formatPnl(delta)}`);
111
+ }
112
+ catch (err) {
113
+ console.error(chalk.red(`Snapshot error: ${err instanceof Error ? err.message : err}`));
114
+ }
115
+ }
116
+ });
117
+ // ── pnl daily ──
118
+ pnl
119
+ .command("daily")
120
+ .description("Daily PnL breakdown")
121
+ .option("--exchange <name>", "Single exchange filter")
122
+ .option("--days <n>", "Number of days to show", "14")
123
+ .option("--since <period>", "Time range (e.g. 7d, 2w, 30d)")
124
+ .action(async (opts) => {
125
+ await withJsonErrors(isJson(), async () => {
126
+ const since = parseSince(opts.since) ?? new Date(Date.now() - parseInt(opts.days) * 86400000);
127
+ const snapshots = readEquityHistory({ exchange: opts.exchange, since });
128
+ if (snapshots.length === 0) {
129
+ if (isJson()) {
130
+ printJson(jsonError("NO_DATA", "No equity snapshots found. Run 'perp pnl snapshot' or 'perp pnl track' first."));
131
+ }
132
+ else {
133
+ console.log(chalk.yellow("No equity snapshots found. Run 'perp pnl snapshot' or 'perp pnl track' first."));
134
+ }
135
+ return;
136
+ }
137
+ const daily = computeDailyPnl(snapshots, opts.exchange);
138
+ if (isJson()) {
139
+ printJson(jsonOk({ daily }));
140
+ }
141
+ else {
142
+ console.log(chalk.cyan.bold(`Daily PnL${opts.exchange ? ` (${opts.exchange})` : ""}\n`));
143
+ const head = ["Date", "Start", "End", "PnL", "PnL %"];
144
+ const rows = daily.map(d => [
145
+ d.date,
146
+ `$${formatUsd(d.startEquity)}`,
147
+ `$${formatUsd(d.endEquity)}`,
148
+ formatPnl(d.pnl),
149
+ `${d.pnlPct >= 0 ? "+" : ""}${d.pnlPct.toFixed(2)}%`,
150
+ ]);
151
+ console.log(makeTable(head, rows));
152
+ }
153
+ });
154
+ });
155
+ // ── pnl weekly ──
156
+ pnl
157
+ .command("weekly")
158
+ .description("Weekly PnL breakdown")
159
+ .option("--exchange <name>", "Single exchange filter")
160
+ .option("--weeks <n>", "Number of weeks to show", "8")
161
+ .option("--since <period>", "Time range (e.g. 4w, 8w)")
162
+ .action(async (opts) => {
163
+ await withJsonErrors(isJson(), async () => {
164
+ const since = parseSince(opts.since) ?? new Date(Date.now() - parseInt(opts.weeks) * 7 * 86400000);
165
+ const snapshots = readEquityHistory({ exchange: opts.exchange, since });
166
+ if (snapshots.length === 0) {
167
+ if (isJson()) {
168
+ printJson(jsonError("NO_DATA", "No equity snapshots found."));
169
+ }
170
+ else {
171
+ console.log(chalk.yellow("No equity snapshots found. Run 'perp pnl snapshot' or 'perp pnl track' first."));
172
+ }
173
+ return;
174
+ }
175
+ const daily = computeDailyPnl(snapshots, opts.exchange);
176
+ const weekly = aggregateWeekly(daily);
177
+ if (isJson()) {
178
+ printJson(jsonOk({ weekly }));
179
+ }
180
+ else {
181
+ console.log(chalk.cyan.bold(`Weekly PnL${opts.exchange ? ` (${opts.exchange})` : ""}\n`));
182
+ const head = ["Week", "Start", "End", "PnL", "PnL %"];
183
+ const rows = weekly.map(w => [
184
+ w.date,
185
+ `$${formatUsd(w.startEquity)}`,
186
+ `$${formatUsd(w.endEquity)}`,
187
+ formatPnl(w.pnl),
188
+ `${w.pnlPct >= 0 ? "+" : ""}${w.pnlPct.toFixed(2)}%`,
189
+ ]);
190
+ console.log(makeTable(head, rows));
191
+ }
192
+ });
193
+ });
194
+ // ── pnl summary ──
195
+ pnl
196
+ .command("summary")
197
+ .description("Performance summary — Sharpe, drawdown, win rate")
198
+ .option("--exchange <name>", "Single exchange filter")
199
+ .option("--since <period>", "Time range (e.g. 7d, 30d, 12w)")
200
+ .action(async (opts) => {
201
+ await withJsonErrors(isJson(), async () => {
202
+ const since = parseSince(opts.since);
203
+ const snapshots = readEquityHistory({ exchange: opts.exchange, since });
204
+ if (snapshots.length === 0) {
205
+ if (isJson()) {
206
+ printJson(jsonError("NO_DATA", "No equity snapshots found."));
207
+ }
208
+ else {
209
+ console.log(chalk.yellow("No equity snapshots found. Run 'perp pnl snapshot' or 'perp pnl track' first."));
210
+ }
211
+ return;
212
+ }
213
+ const metrics = computePnlMetrics(snapshots, opts.exchange);
214
+ if (isJson()) {
215
+ printJson(jsonOk({
216
+ ...metrics,
217
+ dailyReturns: undefined, // omit large array from JSON
218
+ dailyCount: metrics.dailyReturns.length,
219
+ }));
220
+ }
221
+ else {
222
+ console.log(chalk.cyan.bold(`Performance Summary${opts.exchange ? ` (${opts.exchange})` : ""}\n`));
223
+ console.log(` Period: ${metrics.period.from} → ${metrics.period.to} (${metrics.period.days} days)`);
224
+ console.log(` Total Return: ${formatPnl(metrics.totalReturn)} (${metrics.totalReturnPct >= 0 ? "+" : ""}${metrics.totalReturnPct.toFixed(2)}%)`);
225
+ console.log(` Peak Equity: $${formatUsd(metrics.peakEquity)}`);
226
+ console.log(` Avg Daily PnL: ${formatPnl(metrics.avgDailyPnl)}`);
227
+ console.log();
228
+ console.log(` Sharpe Ratio: ${metrics.sharpeRatio >= 0 ? chalk.green(metrics.sharpeRatio.toFixed(2)) : chalk.red(metrics.sharpeRatio.toFixed(2))}`);
229
+ console.log(` Max Drawdown: ${chalk.red(`$${formatUsd(metrics.maxDrawdown)}`)} (${chalk.red(`-${metrics.maxDrawdownPct.toFixed(2)}%`)})`);
230
+ console.log(` Current DD: $${formatUsd(metrics.currentDrawdown)} (-${metrics.currentDrawdownPct.toFixed(2)}%)`);
231
+ console.log();
232
+ console.log(` Win Days: ${chalk.green(String(metrics.winDays))} / ${metrics.winDays + metrics.lossDays}`);
233
+ console.log(` Win Rate: ${metrics.winRate.toFixed(1)}%`);
234
+ if (metrics.bestDay) {
235
+ console.log(` Best Day: ${metrics.bestDay.date} ${formatPnl(metrics.bestDay.pnl)}`);
236
+ }
237
+ if (metrics.worstDay) {
238
+ console.log(` Worst Day: ${metrics.worstDay.date} ${formatPnl(metrics.worstDay.pnl)}`);
239
+ }
240
+ }
241
+ });
242
+ });
243
+ }
@@ -1,3 +1,5 @@
1
1
  import { Command } from "commander";
2
2
  import type { ExchangeAdapter } from "../exchanges/interface.js";
3
+ /** Run health check and return results + render. Exported for use by status --health. */
4
+ export declare function runHealthCheck(isJson: () => boolean): Promise<void>;
3
5
  export declare function registerRiskCommands(program: Command, getAdapterForExchange: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean): void;
@@ -1,6 +1,52 @@
1
1
  import chalk from "chalk";
2
2
  import { printJson, jsonOk, makeTable, formatUsd, withJsonErrors } from "../utils.js";
3
3
  import { loadRiskLimits, saveRiskLimits, assessRisk, getLiquidationDistances } from "../risk.js";
4
+ import { pingPacifica, pingHyperliquid, pingLighter } from "../shared-api.js";
5
+ /** Run health check and return results + render. Exported for use by status --health. */
6
+ export async function runHealthCheck(isJson) {
7
+ const [pacPing, hlPing, ltPing] = await Promise.all([
8
+ pingPacifica(),
9
+ pingHyperliquid(),
10
+ pingLighter(),
11
+ ]);
12
+ const toPingResult = (name, p) => ({
13
+ exchange: name,
14
+ status: p.ok ? "ok" : "down",
15
+ latency_ms: p.latencyMs,
16
+ error: p.ok ? undefined : `HTTP ${p.status}`,
17
+ });
18
+ const checks = [
19
+ toPingResult("pacifica", pacPing),
20
+ toPingResult("hyperliquid", hlPing),
21
+ toPingResult("lighter", ltPing),
22
+ ];
23
+ const allOk = checks.every(c => c.status === "ok");
24
+ if (isJson()) {
25
+ return printJson(jsonOk({ healthy: allOk, exchanges: checks }));
26
+ }
27
+ console.log(chalk.cyan.bold("\n Exchange Health Check\n"));
28
+ const rows = checks.map(c => {
29
+ const statusIcon = c.status === "ok"
30
+ ? chalk.green("OK")
31
+ : c.status === "degraded"
32
+ ? chalk.yellow("DEGRADED")
33
+ : chalk.red("DOWN");
34
+ const latency = c.latency_ms < 500
35
+ ? chalk.green(`${c.latency_ms}ms`)
36
+ : c.latency_ms < 2000
37
+ ? chalk.yellow(`${c.latency_ms}ms`)
38
+ : chalk.red(`${c.latency_ms}ms`);
39
+ return [
40
+ chalk.white.bold(c.exchange),
41
+ statusIcon,
42
+ latency,
43
+ c.error ? chalk.red(c.error) : chalk.gray("-"),
44
+ ];
45
+ });
46
+ console.log(makeTable(["Exchange", "Status", "Latency", "Error"], rows));
47
+ const overall = allOk ? chalk.green("ALL HEALTHY") : chalk.red("ISSUES DETECTED");
48
+ console.log(`\n Overall: ${overall}\n`);
49
+ }
4
50
  const EXCHANGES = ["pacifica", "hyperliquid", "lighter"];
5
51
  export function registerRiskCommands(program, getAdapterForExchange, isJson) {
6
52
  const risk = program.command("risk").description("Risk management and guardrails");
@@ -278,4 +324,14 @@ export function registerRiskCommands(program, getAdapterForExchange, isJson) {
278
324
  }
279
325
  });
280
326
  });
327
+ // ── deprecated: health (merged into risk) ──
328
+ const healthCmd = program
329
+ .command("health")
330
+ .description("Use 'perp status --health'")
331
+ .action(async () => {
332
+ if (!isJson())
333
+ console.log(chalk.yellow(" Use 'perp status --health' instead.\n"));
334
+ await runHealthCheck(isJson);
335
+ });
336
+ healthCmd._hidden = true;
281
337
  }
@@ -1,6 +1,26 @@
1
1
  import chalk from "chalk";
2
2
  import { printJson, jsonOk, withJsonErrors } from "../utils.js";
3
3
  import { loadSettings, saveSettings } from "../settings.js";
4
+ import { ENV_FILE, loadEnvFile, setEnvVar, EXCHANGE_ENV_MAP, validateKey } from "./init.js";
5
+ // Resolve exchange alias → env var key
6
+ function resolveEnvKey(nameOrKey) {
7
+ // Direct env var name
8
+ const upper = nameOrKey.toUpperCase();
9
+ for (const info of Object.values(EXCHANGE_ENV_MAP)) {
10
+ if (info.envKey === upper)
11
+ return { envKey: info.envKey, chain: info.chain };
12
+ }
13
+ // Exchange name alias
14
+ const info = EXCHANGE_ENV_MAP[nameOrKey.toLowerCase()];
15
+ if (info)
16
+ return { envKey: info.envKey, chain: info.chain };
17
+ // Common aliases
18
+ const aliases = { hl: "hyperliquid", pac: "pacifica", lt: "lighter" };
19
+ const aliased = aliases[nameOrKey.toLowerCase()];
20
+ if (aliased)
21
+ return resolveEnvKey(aliased);
22
+ return null;
23
+ }
4
24
  export function registerSettingsCommands(program, isJson, getAdapterForExchange) {
5
25
  const settings = program.command("settings").description("CLI settings (referrals, defaults, fees)");
6
26
  // ── settings show ──
@@ -230,4 +250,131 @@ export function registerSettingsCommands(program, isJson, getAdapterForExchange)
230
250
  console.log(chalk.green(`\n ${key} = ${value}\n`));
231
251
  });
232
252
  });
253
+ // ── settings env ──
254
+ const env = settings.command("env").description("Manage ~/.perp/.env configuration");
255
+ // ── settings env show ──
256
+ env
257
+ .command("show")
258
+ .description("Show current configuration")
259
+ .action(async () => {
260
+ const stored = loadEnvFile();
261
+ const entries = [];
262
+ for (const [exchange, info] of Object.entries(EXCHANGE_ENV_MAP)) {
263
+ const fromFile = stored[info.envKey];
264
+ const fromEnv = process.env[info.envKey];
265
+ if (fromFile) {
266
+ entries.push({ name: exchange, chain: info.chain, key: fromFile, source: "~/.perp/.env" });
267
+ }
268
+ else if (fromEnv) {
269
+ entries.push({ name: exchange, chain: info.chain, key: fromEnv, source: "environment" });
270
+ }
271
+ }
272
+ // Derive addresses
273
+ const results = [];
274
+ for (const entry of entries) {
275
+ const { valid, address } = await validateKey(entry.chain, entry.key);
276
+ results.push({ name: entry.name, address: valid ? address : "(invalid key)", source: entry.source });
277
+ }
278
+ if (isJson()) {
279
+ const data = results.map((r) => ({ exchange: r.name, address: r.address, source: r.source }));
280
+ return printJson(jsonOk({ envFile: ENV_FILE, exchanges: data }));
281
+ }
282
+ console.log(chalk.cyan.bold("\n perp-cli Configuration\n"));
283
+ console.log(` File: ${chalk.gray(ENV_FILE)}\n`);
284
+ if (results.length === 0) {
285
+ console.log(chalk.gray(" No keys configured. Run 'perp init' or 'perp settings env set <exchange> <key>'\n"));
286
+ return;
287
+ }
288
+ for (const { name, address, source } of results) {
289
+ console.log(` ${chalk.cyan(name.padEnd(14))} ${chalk.green(address)} ${chalk.gray(source)}`);
290
+ }
291
+ console.log();
292
+ });
293
+ // ── settings env set <exchange|key> <value> ──
294
+ env
295
+ .command("set <name> <value>")
296
+ .description("Set a key (exchange name or env var name)")
297
+ .action(async (name, value) => {
298
+ const resolved = resolveEnvKey(name);
299
+ if (resolved) {
300
+ // Validate the key
301
+ const { valid, address } = await validateKey(resolved.chain, value);
302
+ if (!valid) {
303
+ if (isJson()) {
304
+ const { jsonError } = await import("../utils.js");
305
+ return printJson(jsonError("INVALID_PARAMS", `Invalid ${resolved.chain} private key`));
306
+ }
307
+ console.error(chalk.red(`\n Invalid ${resolved.chain} private key.\n`));
308
+ process.exit(1);
309
+ }
310
+ const normalized = resolved.chain === "evm"
311
+ ? (value.startsWith("0x") ? value : `0x${value}`)
312
+ : value;
313
+ setEnvVar(resolved.envKey, normalized);
314
+ if (isJson())
315
+ return printJson(jsonOk({ key: resolved.envKey, address, file: ENV_FILE }));
316
+ console.log(chalk.green(`\n ${resolved.envKey} set.`));
317
+ console.log(` Address: ${chalk.gray(address)}`);
318
+ console.log(` File: ${chalk.gray("~/.perp/.env")}\n`);
319
+ }
320
+ else {
321
+ // Raw env var (e.g. LIGHTER_API_KEY, custom vars)
322
+ setEnvVar(name, value);
323
+ if (isJson())
324
+ return printJson(jsonOk({ key: name, file: ENV_FILE }));
325
+ console.log(chalk.green(`\n ${name} set.`));
326
+ console.log(` File: ${chalk.gray("~/.perp/.env")}\n`);
327
+ }
328
+ });
329
+ // ── settings env remove <name> ──
330
+ env
331
+ .command("remove <name>")
332
+ .description("Remove a key from ~/.perp/.env")
333
+ .action(async (name) => {
334
+ const resolved = resolveEnvKey(name);
335
+ const envKey = resolved?.envKey || name;
336
+ const env = loadEnvFile();
337
+ if (!(envKey in env)) {
338
+ if (isJson()) {
339
+ const { jsonError } = await import("../utils.js");
340
+ return printJson(jsonError("NOT_FOUND", `${envKey} not found in ~/.perp/.env`));
341
+ }
342
+ console.log(chalk.gray(`\n ${envKey} not found in ~/.perp/.env\n`));
343
+ return;
344
+ }
345
+ delete env[envKey];
346
+ // Rewrite file
347
+ const { writeFileSync } = await import("fs");
348
+ const lines = ["# perp-cli configuration", "# Generated by 'perp init' — edit freely", ""];
349
+ for (const [k, v] of Object.entries(env))
350
+ lines.push(`${k}=${v}`);
351
+ lines.push("");
352
+ writeFileSync(ENV_FILE, lines.join("\n"), { mode: 0o600 });
353
+ if (isJson())
354
+ return printJson(jsonOk({ removed: envKey }));
355
+ console.log(chalk.yellow(`\n ${envKey} removed from ~/.perp/.env\n`));
356
+ });
357
+ // ── settings env path ──
358
+ env
359
+ .command("path")
360
+ .description("Print env file path")
361
+ .action(() => {
362
+ if (isJson())
363
+ return printJson(jsonOk({ path: ENV_FILE }));
364
+ console.log(ENV_FILE);
365
+ });
366
+ // ── Deprecated top-level `env` command ──
367
+ const deprecatedEnv = program
368
+ .command("env")
369
+ .description("[deprecated] Use 'perp settings env'")
370
+ .allowUnknownOption()
371
+ .action(() => {
372
+ console.log(chalk.yellow("\n 'perp env' is deprecated. Use 'perp settings env' instead.\n"));
373
+ console.log(chalk.gray(" Examples:"));
374
+ console.log(chalk.gray(" perp settings env show"));
375
+ console.log(chalk.gray(" perp settings env set <exchange> <key>"));
376
+ console.log(chalk.gray(" perp settings env remove <name>"));
377
+ console.log(chalk.gray(" perp settings env path\n"));
378
+ });
379
+ deprecatedEnv._hidden = true;
233
380
  }