perp-cli 0.4.0 → 0.4.8

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 (79) hide show
  1. package/README.md +2 -0
  2. package/dist/arb/sizing.d.ts +12 -1
  3. package/dist/arb/sizing.js +38 -5
  4. package/dist/arb/state.d.ts +6 -0
  5. package/dist/arb/utils.d.ts +2 -0
  6. package/dist/arb/utils.js +1 -1
  7. package/dist/cli-spec.d.ts +0 -4
  8. package/dist/cli-spec.js +8 -1
  9. package/dist/commands/agent.js +12 -9
  10. package/dist/commands/arb/index.js +18 -3
  11. package/dist/commands/arb-auto.js +885 -67
  12. package/dist/commands/dex.js +7 -1
  13. package/dist/commands/funding.d.ts +2 -1
  14. package/dist/commands/funding.js +211 -4
  15. package/dist/commands/manage.js +1 -1
  16. package/dist/commands/market.js +1 -1
  17. package/dist/commands/multileg.d.ts +16 -0
  18. package/dist/commands/multileg.js +174 -0
  19. package/dist/commands/pnl.d.ts +12 -0
  20. package/dist/commands/pnl.js +243 -0
  21. package/dist/commands/settings.d.ts +2 -1
  22. package/dist/commands/settings.js +133 -2
  23. package/dist/commands/stream.js +132 -55
  24. package/dist/commands/trade.js +118 -43
  25. package/dist/commands/wallet.js +1 -1
  26. package/dist/config.d.ts +5 -0
  27. package/dist/config.js +12 -0
  28. package/dist/constants.d.ts +14 -0
  29. package/dist/constants.js +40 -0
  30. package/dist/dashboard/ws-feeds.js +1 -1
  31. package/dist/equity-tracker.d.ts +57 -0
  32. package/dist/equity-tracker.js +209 -0
  33. package/dist/exchanges/hyperliquid-spot.d.ts +68 -0
  34. package/dist/exchanges/hyperliquid-spot.js +317 -0
  35. package/dist/exchanges/hyperliquid.d.ts +26 -5
  36. package/dist/exchanges/hyperliquid.js +181 -92
  37. package/dist/exchanges/interface.d.ts +1 -1
  38. package/dist/exchanges/lighter-spot.d.ts +55 -0
  39. package/dist/exchanges/lighter-spot.js +417 -0
  40. package/dist/exchanges/lighter.d.ts +10 -2
  41. package/dist/exchanges/lighter.js +132 -37
  42. package/dist/exchanges/pacifica.d.ts +11 -4
  43. package/dist/exchanges/pacifica.js +52 -37
  44. package/dist/exchanges/spot-interface.d.ts +45 -0
  45. package/dist/exchanges/spot-interface.js +22 -0
  46. package/dist/execution-log.d.ts +2 -2
  47. package/dist/funding/rates.d.ts +36 -0
  48. package/dist/funding/rates.js +146 -0
  49. package/dist/index.js +148 -99
  50. package/dist/liquidity.d.ts +12 -0
  51. package/dist/liquidity.js +43 -0
  52. package/dist/settings.d.ts +6 -0
  53. package/dist/settings.js +18 -2
  54. package/dist/signer/evm-local.d.ts +12 -0
  55. package/dist/signer/evm-local.js +19 -0
  56. package/dist/signer/index.d.ts +3 -0
  57. package/dist/signer/index.js +2 -0
  58. package/dist/signer/interface.d.ts +31 -0
  59. package/dist/signer/interface.js +7 -0
  60. package/dist/signer/solana-local.d.ts +13 -0
  61. package/dist/signer/solana-local.js +19 -0
  62. package/dist/smart-order.d.ts +44 -0
  63. package/dist/smart-order.js +145 -0
  64. package/dist/trade-validator.js +2 -1
  65. package/dist/utils.js +7 -1
  66. package/dist/ws/market-feed.d.ts +74 -0
  67. package/dist/ws/market-feed.js +227 -0
  68. package/package.json +1 -1
  69. package/skills/perp-cli/SKILL.md +102 -4
  70. package/skills/perp-cli/references/commands.md +16 -1
  71. package/skills/perp-cli/references/strategies.md +1 -1
  72. package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
  73. package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
  74. package/skills/perp-cli/scripts/preflight.sh +98 -0
  75. package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
  76. package/skills/perp-cli/scripts/validate-arb.sh +112 -0
  77. package/skills/perp-cli/templates/arb-scan.sh +0 -15
  78. package/skills/perp-cli/templates/open-position.sh +0 -38
  79. package/skills/perp-cli/templates/wallet-setup.sh +0 -17
@@ -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("PnL tracker — equity tracking & performance metrics");
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,2 +1,3 @@
1
1
  import { Command } from "commander";
2
- export declare function registerSettingsCommands(program: Command, isJson: () => boolean): void;
2
+ import type { ExchangeAdapter } from "../exchanges/interface.js";
3
+ export declare function registerSettingsCommands(program: Command, isJson: () => boolean, getAdapterForExchange?: (exchange: string) => Promise<ExchangeAdapter>): void;
@@ -1,8 +1,8 @@
1
1
  import chalk from "chalk";
2
2
  import { printJson, jsonOk, withJsonErrors } from "../utils.js";
3
3
  import { loadSettings, saveSettings } from "../settings.js";
4
- export function registerSettingsCommands(program, isJson) {
5
- const settings = program.command("settings").description("CLI settings (referrals, defaults)");
4
+ export function registerSettingsCommands(program, isJson, getAdapterForExchange) {
5
+ const settings = program.command("settings").description("CLI settings (referrals, defaults, fees)");
6
6
  // ── settings show ──
7
7
  settings
8
8
  .command("show")
@@ -19,6 +19,12 @@ export function registerSettingsCommands(program, isJson) {
19
19
  console.log(` Pacifica: ${s.referralCodes.pacifica || chalk.gray("(none)")}`);
20
20
  console.log(` Hyperliquid: ${s.referralCodes.hyperliquid || chalk.gray("(none)")}`);
21
21
  console.log(` Lighter: ${s.referralCodes.lighter || chalk.gray("(none)")}`);
22
+ console.log(chalk.white.bold("\n Fee Tiers:"));
23
+ for (const [ex, fee] of Object.entries(s.fees)) {
24
+ const tPct = (fee.taker * 100).toFixed(4);
25
+ const mPct = (fee.maker * 100).toFixed(4);
26
+ console.log(` ${ex.padEnd(14)} taker: ${tPct}% maker: ${mPct}%`);
27
+ }
22
28
  console.log(chalk.gray(`\n Config: ~/.perp/settings.json\n`));
23
29
  });
24
30
  });
@@ -58,6 +64,131 @@ export function registerSettingsCommands(program, isJson) {
58
64
  }
59
65
  });
60
66
  });
67
+ // ── settings fees ──
68
+ const fees = settings
69
+ .command("fees")
70
+ .description("Show or sync exchange fee tiers");
71
+ fees
72
+ .command("show")
73
+ .description("Show current fee tiers")
74
+ .action(async () => {
75
+ await withJsonErrors(isJson(), async () => {
76
+ const s = loadSettings();
77
+ if (isJson())
78
+ return printJson(jsonOk(s.fees));
79
+ console.log(chalk.cyan.bold("\n Exchange Fee Tiers\n"));
80
+ for (const [ex, fee] of Object.entries(s.fees)) {
81
+ const tPct = (fee.taker * 100).toFixed(4);
82
+ const mPct = (fee.maker * 100).toFixed(4);
83
+ console.log(` ${ex.padEnd(14)} taker: ${chalk.yellow(tPct + "%")} maker: ${chalk.green(mPct + "%")}`);
84
+ }
85
+ console.log(chalk.gray(`\n Sync from exchanges: perp settings fees sync`));
86
+ console.log(chalk.gray(` Manual set: perp settings fees set <exchange> <taker> <maker>\n`));
87
+ });
88
+ });
89
+ fees
90
+ .command("sync")
91
+ .description("Fetch fee tiers from exchange APIs and save")
92
+ .option("--exchange <name>", "Sync only a specific exchange")
93
+ .action(async (opts) => {
94
+ await withJsonErrors(isJson(), async () => {
95
+ if (!getAdapterForExchange) {
96
+ console.error(chalk.red(" Fee sync requires exchange adapters. Set up keys first with `perp init`."));
97
+ return;
98
+ }
99
+ const s = loadSettings();
100
+ const exchanges = opts.exchange ? [opts.exchange.toLowerCase()] : ["hyperliquid", "pacifica", "lighter"];
101
+ const results = {};
102
+ for (const ex of exchanges) {
103
+ try {
104
+ if (ex === "lighter") {
105
+ // Lighter has 0% fees
106
+ s.fees.lighter = { taker: 0, maker: 0 };
107
+ results.lighter = { taker: 0, maker: 0, source: "known (0% fee exchange)" };
108
+ if (!isJson())
109
+ console.log(chalk.green(` ✓ lighter: taker 0% / maker 0% (no fees)`));
110
+ continue;
111
+ }
112
+ if (ex === "hyperliquid") {
113
+ const adapter = await getAdapterForExchange("hyperliquid");
114
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
115
+ const hlAdapter = adapter;
116
+ if (typeof hlAdapter.getUserFees === "function") {
117
+ const feeData = await hlAdapter.getUserFees();
118
+ // HL returns: { activeReferralDiscount, dailyUserVlm, feeSchedule: { ... }, userCrossRate, userAddRate }
119
+ // userCrossRate is the effective taker fee, userAddRate is the maker fee
120
+ const taker = parseFloat(feeData?.userCrossRate ?? feeData?.takerRate ?? "0.00035");
121
+ const maker = parseFloat(feeData?.userAddRate ?? feeData?.makerRate ?? "0.00002");
122
+ s.fees.hyperliquid = { taker: Math.abs(taker), maker };
123
+ results.hyperliquid = { taker: Math.abs(taker), maker, source: "API (getUserFees)" };
124
+ if (!isJson()) {
125
+ console.log(chalk.green(` ✓ hyperliquid: taker ${(Math.abs(taker) * 100).toFixed(4)}% / maker ${(maker * 100).toFixed(4)}%`));
126
+ }
127
+ }
128
+ else {
129
+ if (!isJson())
130
+ console.log(chalk.yellow(` ⚠ hyperliquid: getUserFees not available, keeping current`));
131
+ }
132
+ continue;
133
+ }
134
+ if (ex === "pacifica") {
135
+ const adapter = await getAdapterForExchange("pacifica");
136
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
137
+ const pacAdapter = adapter;
138
+ if (typeof pacAdapter.sdk?.getAccount === "function" && pacAdapter.publicKey) {
139
+ const info = await pacAdapter.sdk.getAccount(pacAdapter.publicKey);
140
+ const taker = parseFloat(info?.taker_fee ?? "0.00035");
141
+ const maker = parseFloat(info?.maker_fee ?? "0.0001");
142
+ s.fees.pacifica = { taker, maker };
143
+ results.pacifica = { taker, maker, source: "API (getAccount)" };
144
+ if (!isJson()) {
145
+ console.log(chalk.green(` ✓ pacifica: taker ${(taker * 100).toFixed(4)}% / maker ${(maker * 100).toFixed(4)}%`));
146
+ }
147
+ }
148
+ else {
149
+ if (!isJson())
150
+ console.log(chalk.yellow(` ⚠ pacifica: account API not available, keeping current`));
151
+ }
152
+ continue;
153
+ }
154
+ if (!isJson())
155
+ console.log(chalk.yellow(` ⚠ ${ex}: unknown exchange, skipped`));
156
+ }
157
+ catch (err) {
158
+ if (!isJson()) {
159
+ console.log(chalk.red(` ✗ ${ex}: ${err instanceof Error ? err.message : String(err)}`));
160
+ }
161
+ }
162
+ }
163
+ saveSettings(s);
164
+ if (isJson())
165
+ return printJson(jsonOk(results));
166
+ console.log(chalk.gray("\n Fees saved to ~/.perp/settings.json\n"));
167
+ });
168
+ });
169
+ fees
170
+ .command("set")
171
+ .description("Manually set fee tier for an exchange")
172
+ .argument("<exchange>", "Exchange name")
173
+ .argument("<taker>", "Taker fee as percentage (e.g. 0.035 for 0.035%)")
174
+ .argument("<maker>", "Maker fee as percentage (e.g. 0.01 for 0.01%)")
175
+ .action(async (exchange, takerStr, makerStr) => {
176
+ await withJsonErrors(isJson(), async () => {
177
+ const s = loadSettings();
178
+ const ex = exchange.toLowerCase();
179
+ const taker = parseFloat((parseFloat(takerStr) / 100).toPrecision(10));
180
+ const maker = parseFloat((parseFloat(makerStr) / 100).toPrecision(10));
181
+ if (isNaN(taker) || isNaN(maker)) {
182
+ console.error(chalk.red(" Invalid fee values. Use percentages, e.g.: perp settings fees set hyperliquid 0.035 0.002"));
183
+ return;
184
+ }
185
+ s.fees[ex] = { taker, maker };
186
+ saveSettings(s);
187
+ if (isJson())
188
+ return printJson(jsonOk({ exchange: ex, taker, maker }));
189
+ console.log(chalk.green(`\n ${ex}: taker ${takerStr}% / maker ${makerStr}%\n`));
190
+ });
191
+ });
61
192
  // ── settings set ──
62
193
  settings
63
194
  .command("set")
@@ -1,93 +1,170 @@
1
1
  import { PacificaWSClient } from "../pacifica/index.js";
2
2
  import chalk from "chalk";
3
3
  import { formatUsd, formatPercent } from "../utils.js";
4
+ /** Create an HL or Lighter market feed, connected and ready. */
5
+ async function getMarketFeed(exchange) {
6
+ const { createMarketFeed } = await import("../ws/market-feed.js");
7
+ const feed = createMarketFeed(exchange);
8
+ await feed.connect();
9
+ process.on("SIGINT", () => { feed.close(); process.exit(0); });
10
+ return feed;
11
+ }
4
12
  export function registerStreamCommands(program, getNetwork, getExchange, getAdapter) {
5
13
  const stream = program.command("stream").description("Live WebSocket streams (Ctrl+C to stop)");
14
+ // ── stream prices ──
6
15
  stream
7
16
  .command("prices")
8
17
  .description("Stream live prices")
9
18
  .action(async () => {
10
- const ws = new PacificaWSClient({ network: getNetwork() });
11
- await ws.connect();
12
- ws.subscribe("prices");
13
- ws.on("prices", (msg) => {
14
- const data = msg;
15
- const d = data.data ?? data;
16
- if (Array.isArray(d)) {
17
- d.forEach((p) => {
18
- const mark = Number(p.mark ?? p.m ?? 0);
19
- const funding = Number(p.funding ?? p.f ?? 0);
20
- const sym = String(p.symbol ?? p.s ?? "").padEnd(10);
21
- console.log(`${chalk.white.bold(sym)} $${formatUsd(mark)} ${formatPercent(funding)}`);
22
- });
23
- }
24
- });
25
- console.log(chalk.cyan("Streaming prices... (Ctrl+C to stop)\n"));
19
+ const exchange = getExchange();
20
+ if (exchange === "pacifica") {
21
+ const ws = new PacificaWSClient({ network: getNetwork() });
22
+ await ws.connect();
23
+ ws.subscribe("prices");
24
+ ws.on("prices", (msg) => {
25
+ const data = msg;
26
+ const d = data.data ?? data;
27
+ if (Array.isArray(d)) {
28
+ d.forEach((p) => {
29
+ const mark = Number(p.mark ?? p.m ?? 0);
30
+ const funding = Number(p.funding ?? p.f ?? 0);
31
+ const sym = String(p.symbol ?? p.s ?? "").padEnd(10);
32
+ console.log(`${chalk.white.bold(sym)} $${formatUsd(mark)} ${formatPercent(funding)}`);
33
+ });
34
+ }
35
+ });
36
+ console.log(chalk.cyan("Streaming pacifica prices... (Ctrl+C to stop)\n"));
37
+ }
38
+ else {
39
+ const feed = await getMarketFeed(exchange);
40
+ feed.subscribePrices();
41
+ feed.on("prices", (updates) => {
42
+ for (const p of updates) {
43
+ const sym = p.symbol.padEnd(10);
44
+ console.log(`${chalk.white.bold(sym)} $${formatUsd(p.mid)}`);
45
+ }
46
+ });
47
+ console.log(chalk.cyan(`Streaming ${exchange} prices... (Ctrl+C to stop)\n`));
48
+ }
26
49
  await new Promise(() => { }); // keep alive
27
50
  });
51
+ // ── stream book ──
28
52
  stream
29
53
  .command("book <symbol>")
30
54
  .description("Stream orderbook updates")
31
55
  .action(async (symbol) => {
32
- const ws = new PacificaWSClient({ network: getNetwork() });
33
- await ws.connect();
34
- ws.subscribe("book", { symbol: symbol.toUpperCase() });
35
- ws.on("book", (msg) => {
36
- const data = msg;
37
- console.log(`${chalk.cyan(new Date().toLocaleTimeString())} ${JSON.stringify(data.data ?? data)}`);
38
- });
39
- console.log(chalk.cyan(`Streaming ${symbol.toUpperCase()} orderbook... (Ctrl+C to stop)\n`));
56
+ const exchange = getExchange();
57
+ const sym = symbol.toUpperCase();
58
+ if (exchange === "pacifica") {
59
+ const ws = new PacificaWSClient({ network: getNetwork() });
60
+ await ws.connect();
61
+ ws.subscribe("book", { symbol: sym });
62
+ ws.on("book", (msg) => {
63
+ const data = msg;
64
+ console.log(`${chalk.cyan(new Date().toLocaleTimeString())} ${JSON.stringify(data.data ?? data)}`);
65
+ });
66
+ }
67
+ else {
68
+ const feed = await getMarketFeed(exchange);
69
+ feed.subscribeBook(sym);
70
+ feed.on("book", (data) => {
71
+ const topBids = data.bids.slice(0, 5).map(([p, s]) => `${p}x${s}`).join(" ");
72
+ const topAsks = data.asks.slice(0, 5).map(([p, s]) => `${p}x${s}`).join(" ");
73
+ console.log(`${chalk.cyan(new Date().toLocaleTimeString())} ${chalk.green("BID")} ${topBids} ${chalk.red("ASK")} ${topAsks}`);
74
+ });
75
+ }
76
+ console.log(chalk.cyan(`Streaming ${sym} orderbook on ${exchange}... (Ctrl+C to stop)\n`));
40
77
  await new Promise(() => { });
41
78
  });
79
+ // ── stream trades ──
42
80
  stream
43
81
  .command("trades <symbol>")
44
82
  .description("Stream live trades")
45
83
  .action(async (symbol) => {
46
- const ws = new PacificaWSClient({ network: getNetwork() });
47
- await ws.connect();
48
- ws.subscribe("trades", { symbol: symbol.toUpperCase() });
49
- ws.on("trades", (msg) => {
50
- const data = msg;
51
- const d = (data.data ?? data);
52
- const side = String(d.side ?? d.d ?? "");
53
- const price = String(d.price ?? d.p ?? "");
54
- const amount = String(d.amount ?? d.a ?? "");
55
- const color = side === "bid" || side === "buy" ? chalk.green : chalk.red;
56
- console.log(`${chalk.gray(new Date().toLocaleTimeString())} ${color(side === "bid" ? "BUY " : "SELL")} $${formatUsd(price)} x ${amount}`);
57
- });
58
- console.log(chalk.cyan(`Streaming ${symbol.toUpperCase()} trades... (Ctrl+C to stop)\n`));
84
+ const exchange = getExchange();
85
+ const sym = symbol.toUpperCase();
86
+ if (exchange === "pacifica") {
87
+ const ws = new PacificaWSClient({ network: getNetwork() });
88
+ await ws.connect();
89
+ ws.subscribe("trades", { symbol: sym });
90
+ ws.on("trades", (msg) => {
91
+ const data = msg;
92
+ const d = (data.data ?? data);
93
+ const side = String(d.side ?? d.d ?? "");
94
+ const price = String(d.price ?? d.p ?? "");
95
+ const amount = String(d.amount ?? d.a ?? "");
96
+ const color = side === "bid" || side === "buy" ? chalk.green : chalk.red;
97
+ console.log(`${chalk.gray(new Date().toLocaleTimeString())} ${color(side === "bid" ? "BUY " : "SELL")} $${formatUsd(price)} x ${amount}`);
98
+ });
99
+ }
100
+ else {
101
+ const feed = await getMarketFeed(exchange);
102
+ feed.subscribeTrades(sym);
103
+ feed.on("trade", (t) => {
104
+ const color = t.side === "buy" ? chalk.green : chalk.red;
105
+ console.log(`${chalk.gray(new Date().toLocaleTimeString())} ${color(t.side === "buy" ? "BUY " : "SELL")} $${formatUsd(t.price)} x ${t.size}`);
106
+ });
107
+ }
108
+ console.log(chalk.cyan(`Streaming ${sym} trades on ${exchange}... (Ctrl+C to stop)\n`));
59
109
  await new Promise(() => { });
60
110
  });
111
+ // ── stream bbo ──
61
112
  stream
62
113
  .command("bbo <symbol>")
63
114
  .description("Stream best bid/offer")
64
115
  .action(async (symbol) => {
65
- const ws = new PacificaWSClient({ network: getNetwork() });
66
- await ws.connect();
67
- ws.subscribe("bbo", { symbol: symbol.toUpperCase() });
68
- ws.on("bbo", (msg) => {
69
- const data = msg.data ?? msg;
70
- console.log(`${chalk.gray(new Date().toLocaleTimeString())} ${JSON.stringify(data)}`);
71
- });
72
- console.log(chalk.cyan(`Streaming ${symbol.toUpperCase()} BBO... (Ctrl+C to stop)\n`));
116
+ const exchange = getExchange();
117
+ const sym = symbol.toUpperCase();
118
+ if (exchange === "pacifica") {
119
+ const ws = new PacificaWSClient({ network: getNetwork() });
120
+ await ws.connect();
121
+ ws.subscribe("bbo", { symbol: sym });
122
+ ws.on("bbo", (msg) => {
123
+ const data = msg.data ?? msg;
124
+ console.log(`${chalk.gray(new Date().toLocaleTimeString())} ${JSON.stringify(data)}`);
125
+ });
126
+ }
127
+ else {
128
+ // BBO derived from orderbook (HL/Lighter don't have dedicated BBO channel)
129
+ const feed = await getMarketFeed(exchange);
130
+ feed.subscribeBook(sym);
131
+ feed.on("book", (data) => {
132
+ const bid = data.bids[0] ?? ["—", "—"];
133
+ const ask = data.asks[0] ?? ["—", "—"];
134
+ console.log(`${chalk.gray(new Date().toLocaleTimeString())} ${chalk.green("bid")} ${bid[0]} (${bid[1]}) ${chalk.red("ask")} ${ask[0]} (${ask[1]})`);
135
+ });
136
+ }
137
+ console.log(chalk.cyan(`Streaming ${sym} BBO on ${exchange}... (Ctrl+C to stop)\n`));
73
138
  await new Promise(() => { });
74
139
  });
140
+ // ── stream candle ──
75
141
  stream
76
142
  .command("candle <symbol> <interval>")
77
143
  .description("Stream candlestick data")
78
144
  .action(async (symbol, interval) => {
79
- const ws = new PacificaWSClient({ network: getNetwork() });
80
- await ws.connect();
81
- ws.subscribe("candle", { symbol: symbol.toUpperCase(), interval });
82
- ws.on("candle", (msg) => {
83
- const data = msg.data ?? msg;
84
- const d = data;
85
- console.log(`${chalk.gray(new Date().toLocaleTimeString())} O:$${formatUsd(String(d.o ?? ""))} H:$${formatUsd(String(d.h ?? ""))} L:$${formatUsd(String(d.l ?? ""))} C:$${formatUsd(String(d.c ?? ""))} V:${d.v ?? ""}`);
86
- });
87
- console.log(chalk.cyan(`Streaming ${symbol.toUpperCase()} ${interval} candles... (Ctrl+C to stop)\n`));
145
+ const exchange = getExchange();
146
+ const sym = symbol.toUpperCase();
147
+ if (exchange === "pacifica") {
148
+ const ws = new PacificaWSClient({ network: getNetwork() });
149
+ await ws.connect();
150
+ ws.subscribe("candle", { symbol: sym, interval });
151
+ ws.on("candle", (msg) => {
152
+ const data = msg.data ?? msg;
153
+ const d = data;
154
+ console.log(`${chalk.gray(new Date().toLocaleTimeString())} O:$${formatUsd(String(d.o ?? ""))} H:$${formatUsd(String(d.h ?? ""))} L:$${formatUsd(String(d.l ?? ""))} C:$${formatUsd(String(d.c ?? ""))} V:${d.v ?? ""}`);
155
+ });
156
+ }
157
+ else {
158
+ const feed = await getMarketFeed(exchange);
159
+ feed.subscribeCandle(sym, interval);
160
+ feed.on("candle", (d) => {
161
+ console.log(`${chalk.gray(new Date().toLocaleTimeString())} O:$${formatUsd(d.o)} H:$${formatUsd(d.h)} L:$${formatUsd(d.l)} C:$${formatUsd(d.c)} V:${d.v}`);
162
+ });
163
+ }
164
+ console.log(chalk.cyan(`Streaming ${sym} ${interval} candles on ${exchange}... (Ctrl+C to stop)\n`));
88
165
  await new Promise(() => { });
89
166
  });
90
- // ── stream events ── (unified account events as NDJSON)
167
+ // ── stream events ── (unified account events as NDJSON, already exchange-agnostic)
91
168
  if (getAdapter) {
92
169
  stream
93
170
  .command("events")