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
@@ -1,6 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import { HyperliquidAdapter } from "../exchanges/hyperliquid.js";
3
- import { printJson, jsonOk, makeTable, formatUsd, withJsonErrors } from "../utils.js";
3
+ import { printJson, jsonOk, jsonError, makeTable, formatUsd, withJsonErrors } from "../utils.js";
4
4
  export function registerDexCommands(program, getAdapter, isJson) {
5
5
  const dex = program.command("dex").description("HIP-3 deployed perp dex commands (Hyperliquid)");
6
6
  // ── dex list ──
@@ -11,6 +11,8 @@ export function registerDexCommands(program, getAdapter, isJson) {
11
11
  await withJsonErrors(isJson(), async () => {
12
12
  const adapter = await getAdapter();
13
13
  if (!(adapter instanceof HyperliquidAdapter)) {
14
+ if (isJson())
15
+ return printJson(jsonError("INVALID_EXCHANGE", "HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid."));
14
16
  console.error(chalk.red("\n HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid.\n"));
15
17
  return;
16
18
  }
@@ -41,6 +43,8 @@ export function registerDexCommands(program, getAdapter, isJson) {
41
43
  await withJsonErrors(isJson(), async () => {
42
44
  const adapter = await getAdapter();
43
45
  if (!(adapter instanceof HyperliquidAdapter)) {
46
+ if (isJson())
47
+ return printJson(jsonError("INVALID_EXCHANGE", "HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid."));
44
48
  console.error(chalk.red("\n HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid.\n"));
45
49
  return;
46
50
  }
@@ -80,6 +84,8 @@ export function registerDexCommands(program, getAdapter, isJson) {
80
84
  await withJsonErrors(isJson(), async () => {
81
85
  const adapter = await getAdapter();
82
86
  if (!(adapter instanceof HyperliquidAdapter)) {
87
+ if (isJson())
88
+ return printJson(jsonError("INVALID_EXCHANGE", "HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid."));
83
89
  console.error(chalk.red("\n HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid.\n"));
84
90
  return;
85
91
  }
@@ -1,2 +1,3 @@
1
1
  import { Command } from "commander";
2
- export declare function registerFundingCommands(program: Command, isJson: () => boolean): void;
2
+ import type { ExchangeAdapter } from "../exchanges/interface.js";
3
+ export declare function registerFundingCommands(program: Command, isJson: () => boolean, getAdapterForExchange?: (exchange: string) => Promise<ExchangeAdapter>): void;
@@ -1,9 +1,10 @@
1
1
  import chalk from "chalk";
2
- import { makeTable, formatPercent, formatUsd, printJson, jsonOk } from "../utils.js";
2
+ import { makeTable, formatPercent, formatUsd, formatPnl, printJson, jsonOk, jsonError, withJsonErrors } from "../utils.js";
3
3
  import { fetchAllFundingRates, fetchSymbolFundingRates, TOP_SYMBOLS, } from "../funding-rates.js";
4
- import { annualizeHourlyRate } from "../funding.js";
4
+ import { estimateHourlyFunding, annualizeHourlyRate, annualizeRate, toHourlyRate } from "../funding.js";
5
5
  import { saveFundingSnapshot, getHistoricalRates, getCompoundedAnnualReturn, getExchangeCompoundingHours, } from "../funding-history.js";
6
- export function registerFundingCommands(program, isJson) {
6
+ const ALL_EXCHANGES = ["hyperliquid", "pacifica", "lighter"];
7
+ export function registerFundingCommands(program, isJson, getAdapterForExchange) {
7
8
  const funding = program.command("funding").description("Funding rate comparison across exchanges");
8
9
  // ── perp funding rates ── (default: show top symbols)
9
10
  funding
@@ -38,8 +39,15 @@ export function registerFundingCommands(program, isJson) {
38
39
  saveFundingSnapshot(allRates);
39
40
  }
40
41
  catch { /* non-critical */ }
41
- if (isJson())
42
+ if (isJson()) {
43
+ // When --ndjson or --fields is used, return the symbols array directly
44
+ // so each symbol gets its own line / fields can be picked per element.
45
+ // Otherwise return the full snapshot (includes exchangeStatus metadata).
46
+ if (process.argv.includes("--ndjson") || process.argv.includes("--fields")) {
47
+ return printJson(jsonOk(snapshot.symbols));
48
+ }
42
49
  return printJson(jsonOk(snapshot));
50
+ }
43
51
  printSnapshotTable(snapshot);
44
52
  printExchangeStatus(snapshot);
45
53
  console.log(chalk.gray(" * Rates shown are current predictions. Actual settled rates may differ.\n"));
@@ -234,6 +242,205 @@ export function registerFundingCommands(program, isJson) {
234
242
  await new Promise(r => setTimeout(r, intervalSec * 1000));
235
243
  }
236
244
  });
245
+ // ── perp funding positions ── (funding impact on open positions)
246
+ funding
247
+ .command("positions")
248
+ .description("Show funding rate impact on your current positions")
249
+ .option("--exchanges <list>", "Comma-separated exchanges to check (default: all with keys)")
250
+ .action(async (opts) => {
251
+ if (!getAdapterForExchange) {
252
+ const msg = "funding positions requires adapter access";
253
+ if (isJson())
254
+ return printJson(jsonError("INTERNAL", msg));
255
+ console.log(chalk.red(` ${msg}\n`));
256
+ return;
257
+ }
258
+ await withJsonErrors(isJson(), async () => {
259
+ const targetExchanges = opts.exchanges
260
+ ? opts.exchanges.split(",").map(s => s.trim().toLowerCase())
261
+ : [...ALL_EXCHANGES];
262
+ if (!isJson())
263
+ console.log(chalk.cyan(" Fetching positions and funding rates...\n"));
264
+ const results = [];
265
+ const exchangeErrors = {};
266
+ await Promise.all(targetExchanges.map(async (exchange) => {
267
+ try {
268
+ const adapter = await getAdapterForExchange(exchange);
269
+ const [positions, markets, fundingPayments] = await Promise.all([
270
+ adapter.getPositions(),
271
+ adapter.getMarkets(),
272
+ adapter.getFundingPayments(100).catch(() => []),
273
+ ]);
274
+ if (positions.length === 0)
275
+ return;
276
+ // Build symbol → funding rate map
277
+ const fundingMap = new Map();
278
+ for (const m of markets) {
279
+ const sym = m.symbol.toUpperCase().replace(/-PERP$/, "");
280
+ fundingMap.set(sym, {
281
+ rate: Number(m.fundingRate) || 0,
282
+ markPrice: Number(m.markPrice) || 0,
283
+ });
284
+ }
285
+ // Aggregate actual funding payments per symbol (last 24h)
286
+ const now = Date.now();
287
+ const dayAgo = now - 24 * 60 * 60 * 1000;
288
+ const actualFunding = new Map();
289
+ for (const fp of fundingPayments) {
290
+ if (fp.time < dayAgo)
291
+ continue;
292
+ const sym = fp.symbol.toUpperCase().replace(/-PERP$/, "");
293
+ const amt = Number(fp.payment) || 0;
294
+ if (!actualFunding.has(sym))
295
+ actualFunding.set(sym, { received: 0, paid: 0 });
296
+ const entry = actualFunding.get(sym);
297
+ if (amt > 0)
298
+ entry.received += amt;
299
+ else
300
+ entry.paid += Math.abs(amt);
301
+ }
302
+ for (const pos of positions) {
303
+ const sym = pos.symbol.toUpperCase().replace(/-PERP$/, "");
304
+ const fdata = fundingMap.get(sym);
305
+ const fundingRate = fdata?.rate ?? 0;
306
+ const mark = Number(pos.markPrice) || fdata?.markPrice || 0;
307
+ const size = Math.abs(Number(pos.size));
308
+ const notionalUsd = size * mark;
309
+ const hourlyRate = toHourlyRate(fundingRate, exchange);
310
+ const annualPct = annualizeRate(fundingRate, exchange);
311
+ const hourlyPayment = estimateHourlyFunding(fundingRate, exchange, notionalUsd, pos.side);
312
+ const dailyPayment = hourlyPayment * 24;
313
+ const actual = actualFunding.get(sym);
314
+ const actualReceived24h = actual?.received ?? 0;
315
+ const actualPaid24h = actual?.paid ?? 0;
316
+ const actualNet24h = actualReceived24h - actualPaid24h;
317
+ results.push({
318
+ exchange,
319
+ symbol: sym,
320
+ side: pos.side,
321
+ size: pos.size,
322
+ entryPrice: pos.entryPrice,
323
+ markPrice: pos.markPrice,
324
+ unrealizedPnl: pos.unrealizedPnl,
325
+ leverage: pos.leverage,
326
+ notionalUsd,
327
+ fundingRate,
328
+ hourlyRate,
329
+ annualPct,
330
+ hourlyPayment,
331
+ dailyPayment,
332
+ actualReceived24h,
333
+ actualPaid24h,
334
+ actualNet24h,
335
+ });
336
+ }
337
+ }
338
+ catch (err) {
339
+ exchangeErrors[exchange] = err instanceof Error ? err.message : String(err);
340
+ }
341
+ }));
342
+ if (results.length === 0) {
343
+ if (isJson()) {
344
+ return printJson(jsonOk({
345
+ positions: [],
346
+ totals: {
347
+ predicted: { hourly: 0, daily: 0 },
348
+ actual24h: { net: 0 },
349
+ notionalUsd: 0,
350
+ },
351
+ errors: exchangeErrors,
352
+ }));
353
+ }
354
+ console.log(chalk.gray(" No open positions found on any exchange.\n"));
355
+ if (Object.keys(exchangeErrors).length > 0) {
356
+ for (const [ex, err] of Object.entries(exchangeErrors)) {
357
+ console.log(chalk.yellow(` ${ex}: ${err}`));
358
+ }
359
+ console.log();
360
+ }
361
+ return;
362
+ }
363
+ // Sort: biggest daily payment impact first
364
+ results.sort((a, b) => Math.abs(b.dailyPayment) - Math.abs(a.dailyPayment));
365
+ const totalHourly = results.reduce((s, r) => s + r.hourlyPayment, 0);
366
+ const totalDaily = results.reduce((s, r) => s + r.dailyPayment, 0);
367
+ const totalNotional = results.reduce((s, r) => s + r.notionalUsd, 0);
368
+ const totalActualNet = results.reduce((s, r) => s + r.actualNet24h, 0);
369
+ if (isJson()) {
370
+ return printJson(jsonOk({
371
+ positions: results.map(r => ({
372
+ exchange: r.exchange,
373
+ symbol: r.symbol,
374
+ side: r.side,
375
+ size: r.size,
376
+ notionalUsd: Number(r.notionalUsd.toFixed(2)),
377
+ fundingRate: r.fundingRate,
378
+ annualPct: Number(r.annualPct.toFixed(2)),
379
+ predicted: {
380
+ hourly: Number(r.hourlyPayment.toFixed(6)),
381
+ daily: Number(r.dailyPayment.toFixed(4)),
382
+ },
383
+ actual24h: {
384
+ received: Number(r.actualReceived24h.toFixed(6)),
385
+ paid: Number(r.actualPaid24h.toFixed(6)),
386
+ net: Number(r.actualNet24h.toFixed(6)),
387
+ },
388
+ unrealizedPnl: r.unrealizedPnl,
389
+ })),
390
+ totals: {
391
+ predicted: {
392
+ hourly: Number(totalHourly.toFixed(6)),
393
+ daily: Number(totalDaily.toFixed(4)),
394
+ },
395
+ actual24h: {
396
+ net: Number(totalActualNet.toFixed(6)),
397
+ },
398
+ notionalUsd: Number(totalNotional.toFixed(2)),
399
+ },
400
+ errors: Object.keys(exchangeErrors).length > 0 ? exchangeErrors : undefined,
401
+ }));
402
+ }
403
+ const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : e.toUpperCase();
404
+ console.log(chalk.cyan.bold(" Position Funding Impact\n"));
405
+ const rows = results.map(r => {
406
+ const sideColor = r.side === "long" ? chalk.green : chalk.red;
407
+ const rateColor = r.fundingRate > 0 ? chalk.red : r.fundingRate < 0 ? chalk.green : chalk.white;
408
+ // actual net: positive = received, negative = paid
409
+ const actualColor = r.actualNet24h > 0 ? chalk.green : r.actualNet24h < 0 ? chalk.red : chalk.gray;
410
+ const actualStr = r.actualNet24h !== 0
411
+ ? actualColor(`${r.actualNet24h > 0 ? "+" : ""}$${r.actualNet24h.toFixed(4)}`)
412
+ : chalk.gray("-");
413
+ return [
414
+ chalk.white.bold(exAbbr(r.exchange)),
415
+ chalk.white.bold(r.symbol),
416
+ sideColor(r.side.toUpperCase()),
417
+ `$${formatUsd(r.notionalUsd)}`,
418
+ rateColor(formatPercent(r.fundingRate)),
419
+ rateColor(`${r.annualPct.toFixed(1)}%`),
420
+ actualStr,
421
+ formatPnl(r.unrealizedPnl),
422
+ ];
423
+ });
424
+ console.log(makeTable(["Ex", "Symbol", "Side", "Notional", "Rate(now)", "Annual(now)", "Actual 24h", "uPnL"], rows));
425
+ // Summary
426
+ const actualNetColor = totalActualNet >= 0 ? chalk.green : chalk.red;
427
+ const predictColor = totalDaily >= 0 ? chalk.red : chalk.green;
428
+ const predictSign = totalDaily >= 0 ? "-" : "+";
429
+ console.log();
430
+ console.log(` Total Notional: $${formatUsd(totalNotional)}`);
431
+ console.log(` Actual Net (24h): ${actualNetColor(`${totalActualNet >= 0 ? "+" : ""}$${totalActualNet.toFixed(4)}`)}`);
432
+ console.log(` Predicted (now rate): ${predictColor(`${predictSign}$${Math.abs(totalDaily).toFixed(4)}/d`)}`);
433
+ if (Object.keys(exchangeErrors).length > 0) {
434
+ console.log();
435
+ for (const [ex, err] of Object.entries(exchangeErrors)) {
436
+ console.log(chalk.yellow(` ${exAbbr(ex)}: ${err}`));
437
+ }
438
+ }
439
+ console.log(chalk.gray("\n Actual 24h: real funding received/paid in the last 24 hours."));
440
+ console.log(chalk.gray(" Predicted: based on current rate only (changes every hour)."));
441
+ console.log(chalk.gray(" + = received, - = paid\n"));
442
+ });
443
+ });
237
444
  }
238
445
  // ── Display helpers ──
239
446
  function formatAvgRate(rate) {
@@ -274,7 +274,7 @@ export function registerManageCommands(program, getAdapter, isJson, getPacificaA
274
274
  manage
275
275
  .command("setup-api-key")
276
276
  .description("Generate & register a Lighter API key (required for trading)")
277
- .option("--key-index <n>", "API key index (2-254, default: 2)", "2")
277
+ .option("--key-index <n>", "API key index (4-254, default: 4)", "4")
278
278
  .action(async (opts) => {
279
279
  const adapter = await getAdapter();
280
280
  const { LighterAdapter } = await import("../exchanges/lighter.js");
@@ -139,7 +139,7 @@ export function registerMarketCommands(program, getAdapter, isJson) {
139
139
  const rows = history.map((h) => [
140
140
  new Date(h.time).toLocaleString(),
141
141
  formatPercent(h.rate),
142
- `$${formatUsd(h.price)}`,
142
+ h.price != null ? `$${formatUsd(h.price)}` : chalk.gray("n/a"),
143
143
  ]);
144
144
  console.log(makeTable(["Time", "Funding Rate", "Oracle"], rows));
145
145
  });
@@ -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;
@@ -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;