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
@@ -12,7 +12,7 @@ function pac(adapter) {
12
12
  throw new Error("This command requires --exchange pacifica");
13
13
  return adapter;
14
14
  }
15
- export function registerTradeCommands(program, getAdapter, isJson, isDryRun = () => false) {
15
+ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = () => false, getAdapterForExchange) {
16
16
  /** Guard: if --dry-run is active, log the intended action and return without executing. */
17
17
  function dryRunGuard(action, details) {
18
18
  if (!isDryRun())
@@ -29,11 +29,11 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
29
29
  console.log();
30
30
  }
31
31
  logExecution({
32
- type: action.includes("limit") ? "limit_order" : action.includes("stop") ? "stop_order" : action.includes("cancel") ? "cancel" : "market_order",
32
+ type: action.includes("split") ? "split_order" : action.includes("limit") ? "limit_order" : action.includes("stop") ? "stop_order" : action.includes("cancel") ? "cancel" : "market_order",
33
33
  exchange: details.exchange ?? "unknown",
34
34
  symbol: (details.symbol ?? "").toUpperCase(),
35
35
  side: details.side ?? "",
36
- size: String(details.size ?? "0"),
36
+ size: String(details.size ?? details.totalUsd ?? "0"),
37
37
  price: details.price,
38
38
  status: "simulated",
39
39
  dryRun: true,
@@ -48,12 +48,37 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
48
48
  .option("-s, --slippage <pct>", "Slippage percent", "1")
49
49
  .option("--reduce-only", "Reduce only order")
50
50
  .option("--smart", "Smart execution: IOC limit at best bid/ask + 1 tick (reduces slippage)")
51
+ .option("--split", "Use orderbook-aware split execution for large orders")
52
+ .option("--max-slippage <pct>", "Max slippage per split slice (%)", "0.3")
51
53
  .option("--client-id <id>", "Client order ID for idempotent tracking")
52
54
  .option("--auto-id", "Auto-generate a client order ID")
53
55
  .action(async (symbol, side, size, opts) => {
54
56
  const s = side.toLowerCase();
55
57
  if (s !== "buy" && s !== "sell")
56
58
  errorAndExit("Side must be buy or sell");
59
+ const sym = symbol.toUpperCase();
60
+ // ── Split execution branch ──
61
+ if (opts.split) {
62
+ const adapter = await getAdapter();
63
+ if (dryRunGuard("split-order", { exchange: adapter.name, symbol: sym, side: s, size }))
64
+ return;
65
+ const { runSplitOrder } = await import("../strategies/split-order.js");
66
+ const markets = await adapter.getMarkets();
67
+ const market = markets.find(m => symbolMatch(m.symbol, sym));
68
+ const markPrice = market ? parseFloat(market.markPrice) : 0;
69
+ const notionalUsd = markPrice > 0 ? parseFloat(size) * markPrice : parseFloat(size);
70
+ const result = await runSplitOrder(adapter, {
71
+ symbol: sym,
72
+ side: s,
73
+ totalSizeUsd: notionalUsd,
74
+ maxSlippagePct: parseFloat(opts.maxSlippage ?? "0.3"),
75
+ }, isJson() ? () => { } : (msg) => console.log(chalk.gray(` ${msg}`)));
76
+ if (isJson())
77
+ return printJson(jsonOk(result));
78
+ const statusColor = result.status === "complete" ? chalk.green : result.status === "partial" ? chalk.yellow : chalk.red;
79
+ console.log(`\n ${statusColor(result.status.toUpperCase())} | ${result.slices.length} slices | $${formatUsd(result.filledUsd)} filled | slippage: ${result.totalSlippagePct.toFixed(3)}%\n`);
80
+ return;
81
+ }
57
82
  const clientId = opts.autoId ? generateClientId() : opts.clientId;
58
83
  if (clientId && isOrderDuplicate(clientId)) {
59
84
  if (isJson())
@@ -62,7 +87,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
62
87
  return;
63
88
  }
64
89
  const adapter = await getAdapter();
65
- if (dryRunGuard("market_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: s, size, smart: !!opts.smart }))
90
+ if (dryRunGuard("market_order", { exchange: adapter.name, symbol: sym, side: s, size, smart: !!opts.smart }))
66
91
  return;
67
92
  if (clientId) {
68
93
  logClientId({
@@ -186,6 +211,47 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
186
211
  console.log(chalk.green(`\n Market SELL ${size} ${symbol.toUpperCase()} placed on ${adapter.name}.${opts.smart ? " (smart)" : ""}\n`));
187
212
  printJson(jsonOk(result));
188
213
  });
214
+ // ── Split Order (orderbook-aware) ──
215
+ trade
216
+ .command("split <symbol> <side> <usd>")
217
+ .description("Orderbook-aware split execution — breaks large orders into slices based on depth")
218
+ .option("--max-slippage <pct>", "Max slippage per slice (%)", "0.3")
219
+ .option("--max-slices <n>", "Max number of slices", "10")
220
+ .option("--delay <ms>", "Delay between slices in ms", "1000")
221
+ .option("--min-slice <usd>", "Minimum slice size in USD", "100")
222
+ .action(async (symbol, side, usd, opts) => {
223
+ const sym = symbol.toUpperCase();
224
+ const orderSide = side.toLowerCase();
225
+ const totalUsd = parseFloat(usd);
226
+ if (!["buy", "sell"].includes(orderSide))
227
+ errorAndExit("Side must be 'buy' or 'sell'");
228
+ if (isNaN(totalUsd) || totalUsd <= 0)
229
+ errorAndExit("USD amount must be > 0");
230
+ const exchange = program.opts().exchange ?? "unknown";
231
+ if (dryRunGuard("split-order", { exchange, symbol: sym, side: orderSide, totalUsd, ...opts }))
232
+ return;
233
+ const adapter = await getAdapter();
234
+ const { runSplitOrder } = await import("../strategies/split-order.js");
235
+ const result = await runSplitOrder(adapter, {
236
+ symbol: sym,
237
+ side: orderSide,
238
+ totalSizeUsd: totalUsd,
239
+ maxSlippagePct: parseFloat(opts.maxSlippage),
240
+ maxSlices: parseInt(opts.maxSlices),
241
+ delayMs: parseInt(opts.delay),
242
+ minSliceUsd: parseFloat(opts.minSlice),
243
+ }, isJson() ? () => { } : (msg) => console.log(chalk.gray(` ${msg}`)));
244
+ if (isJson())
245
+ return printJson(jsonOk(result));
246
+ const statusColor = result.status === "complete" ? chalk.green
247
+ : result.status === "partial" ? chalk.yellow
248
+ : chalk.red;
249
+ console.log(`\n ${statusColor(result.status.toUpperCase())} | ${result.slices.length} slices`);
250
+ console.log(` Filled: $${formatUsd(result.filledUsd)} / $${formatUsd(result.requestedUsd)}`);
251
+ console.log(` Avg Price: $${formatUsd(result.avgPrice)}`);
252
+ console.log(` Slippage: ${result.totalSlippagePct.toFixed(3)}%`);
253
+ console.log(` Runtime: ${result.runtime.toFixed(1)}s\n`);
254
+ });
189
255
  trade
190
256
  .command("limit <symbol> <side> <price> <size>")
191
257
  .description("Place a limit order")
@@ -245,21 +311,42 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
245
311
  printJson(jsonOk(result));
246
312
  });
247
313
  trade
248
- .command("cancel <symbol> <orderId>")
249
- .description("Cancel a specific order")
250
- .action(async (symbol, orderId) => {
314
+ .command("cancel <symbolOrOrderId> [orderId]")
315
+ .description("Cancel an order by orderId (auto-detects symbol), or by symbol + orderId")
316
+ .action(async (symbolOrOrderId, orderId) => {
251
317
  const adapter = await getAdapter();
252
- if (dryRunGuard("cancel", { exchange: adapter.name, symbol: symbol.toUpperCase(), orderId }))
318
+ let symbol;
319
+ let oid;
320
+ if (orderId) {
321
+ // cancel <symbol> <orderId>
322
+ symbol = symbolOrOrderId.toUpperCase();
323
+ oid = orderId;
324
+ }
325
+ else {
326
+ // cancel <orderId> — look up symbol from open orders
327
+ oid = symbolOrOrderId;
328
+ const orders = await adapter.getOpenOrders();
329
+ const match = orders.find((o) => String(o.orderId) === oid);
330
+ if (!match) {
331
+ const err = `Order ${oid} not found in open orders`;
332
+ if (isJson())
333
+ return printJson(jsonOk({ cancelled: false, reason: err }));
334
+ console.log(chalk.yellow(`\n ${err}\n`));
335
+ return;
336
+ }
337
+ symbol = match.symbol;
338
+ }
339
+ if (dryRunGuard("cancel", { exchange: adapter.name, symbol, orderId: oid }))
253
340
  return;
254
341
  try {
255
- const result = await adapter.cancelOrder(symbol.toUpperCase(), orderId);
256
- logExecution({ type: "cancel", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "cancel", size: "0", status: "success", dryRun: false, meta: { orderId } });
342
+ const result = await adapter.cancelOrder(symbol, oid);
343
+ logExecution({ type: "cancel", exchange: adapter.name, symbol, side: "cancel", size: "0", status: "success", dryRun: false, meta: { orderId: oid } });
257
344
  if (isJson())
258
345
  return printJson(jsonOk(result));
259
- console.log(chalk.green(`\n Order ${orderId} cancelled on ${adapter.name}.\n`));
346
+ console.log(chalk.green(`\n Order ${oid} cancelled on ${adapter.name}.\n`));
260
347
  }
261
348
  catch (err) {
262
- logExecution({ type: "cancel", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "cancel", size: "0", status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err), meta: { orderId } });
349
+ logExecution({ type: "cancel", exchange: adapter.name, symbol, side: "cancel", size: "0", status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err), meta: { orderId: oid } });
263
350
  throw err;
264
351
  }
265
352
  });
@@ -601,120 +688,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
601
688
  throw err;
602
689
  }
603
690
  });
604
- // ── Grid Bot (shortcut with --background) ──
605
- trade
606
- .command("grid <symbol>")
607
- .description("Run grid trading bot (foreground or --background)")
608
- .requiredOption("--upper <price>", "Upper price bound")
609
- .requiredOption("--lower <price>", "Lower price bound")
610
- .option("--grids <n>", "Number of grid lines", "10")
611
- .option("--size <size>", "Total position size (base)", "0.1")
612
- .option("--side <side>", "Grid bias: long, short, neutral", "neutral")
613
- .option("--leverage <n>", "Leverage to set")
614
- .option("--interval <sec>", "Check interval in seconds", "10")
615
- .option("--max-runtime <sec>", "Max runtime in seconds (0 = forever)", "0")
616
- .option("--trailing-stop <pct>", "Stop if equity drops by this % from peak")
617
- .option("--background", "Run in background (tmux)")
618
- .action(async (symbol, opts) => {
619
- const exchange = (await getAdapter()).name;
620
- const cliArgs = [
621
- `-e`, exchange, symbol.toUpperCase(),
622
- `--upper`, opts.upper, `--lower`, opts.lower,
623
- `--grids`, opts.grids, `--size`, opts.size,
624
- `--side`, opts.side, `--interval`, opts.interval,
625
- `--max-runtime`, opts.maxRuntime,
626
- ...(opts.leverage ? [`--leverage`, opts.leverage] : []),
627
- ...(opts.trailingStop ? [`--trailing-stop`, opts.trailingStop] : []),
628
- ];
629
- if (opts.background) {
630
- const { startJob } = await import("../jobs.js");
631
- const job = startJob({
632
- strategy: "grid",
633
- exchange,
634
- params: { symbol: symbol.toUpperCase(), ...opts },
635
- cliArgs,
636
- });
637
- if (isJson())
638
- return printJson(jsonOk(job));
639
- console.log(chalk.green(`\n Grid bot started in background.`));
640
- console.log(` ID: ${chalk.white.bold(job.id)}`);
641
- console.log(` Range: $${opts.lower} - $${opts.upper} | ${opts.grids} grids`);
642
- console.log(` Logs: ${chalk.gray(`perp jobs logs ${job.id}`)}`);
643
- console.log(` Stop: ${chalk.gray(`perp jobs stop ${job.id}`)}\n`);
644
- return;
645
- }
646
- // Foreground: delegate to `run grid`
647
- const { runGrid } = await import("../strategies/grid.js");
648
- const adapter = await getAdapter();
649
- const log = (msg) => {
650
- const ts = new Date().toLocaleTimeString();
651
- console.log(`${chalk.gray(ts)} ${msg}`);
652
- };
653
- await runGrid(adapter, {
654
- symbol: symbol.toUpperCase(),
655
- side: opts.side,
656
- upperPrice: parseFloat(opts.upper),
657
- lowerPrice: parseFloat(opts.lower),
658
- grids: parseInt(opts.grids),
659
- totalSize: parseFloat(opts.size),
660
- leverage: opts.leverage ? parseInt(opts.leverage) : undefined,
661
- intervalSec: parseInt(opts.interval),
662
- maxRuntime: parseInt(opts.maxRuntime),
663
- trailingStop: opts.trailingStop ? parseFloat(opts.trailingStop) : undefined,
664
- }, undefined, log);
665
- });
666
- // ── DCA (shortcut with --background) ──
667
- trade
668
- .command("dca <symbol> <side> <amount> <interval>")
669
- .description("Run DCA strategy (foreground or --background)")
670
- .option("--orders <n>", "Total number of orders (0 = unlimited)", "0")
671
- .option("--price-limit <price>", "Stop buying above / selling below this price")
672
- .option("--max-runtime <sec>", "Max runtime in seconds (0 = forever)", "0")
673
- .option("--background", "Run in background (tmux)")
674
- .action(async (symbol, side, amount, interval, opts) => {
675
- const s = side.toLowerCase();
676
- if (s !== "buy" && s !== "sell")
677
- errorAndExit("Side must be buy or sell");
678
- const exchange = (await getAdapter()).name;
679
- const cliArgs = [
680
- `-e`, exchange, symbol.toUpperCase(), s, amount, interval,
681
- `--orders`, opts.orders, `--max-runtime`, opts.maxRuntime,
682
- ...(opts.priceLimit ? [`--price-limit`, opts.priceLimit] : []),
683
- ];
684
- if (opts.background) {
685
- const { startJob } = await import("../jobs.js");
686
- const job = startJob({
687
- strategy: "dca",
688
- exchange,
689
- params: { symbol: symbol.toUpperCase(), side: s, amount, interval, ...opts },
690
- cliArgs,
691
- });
692
- if (isJson())
693
- return printJson(jsonOk(job));
694
- console.log(chalk.green(`\n DCA started in background.`));
695
- console.log(` ID: ${chalk.white.bold(job.id)}`);
696
- console.log(` ${s.toUpperCase()} ${amount} ${symbol.toUpperCase()} every ${interval}s`);
697
- console.log(` Logs: ${chalk.gray(`perp jobs logs ${job.id}`)}`);
698
- console.log(` Stop: ${chalk.gray(`perp jobs stop ${job.id}`)}\n`);
699
- return;
700
- }
701
- // Foreground
702
- const { runDCA } = await import("../strategies/dca.js");
703
- const adapter = await getAdapter();
704
- const log = (msg) => {
705
- const ts = new Date().toLocaleTimeString();
706
- console.log(`${chalk.gray(ts)} ${msg}`);
707
- };
708
- await runDCA(adapter, {
709
- symbol: symbol.toUpperCase(),
710
- side: s,
711
- amountPerOrder: parseFloat(amount),
712
- intervalSec: parseInt(interval),
713
- totalOrders: parseInt(opts.orders),
714
- priceLimit: opts.priceLimit ? parseFloat(opts.priceLimit) : undefined,
715
- maxRuntime: parseInt(opts.maxRuntime),
716
- }, undefined, log);
717
- });
691
+ // grid/dca moved to 'perp bot grid' / 'perp bot dca'
718
692
  // ── Position Management Shortcuts ──
719
693
  trade
720
694
  .command("close-all")
@@ -1103,147 +1077,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
1103
1077
  }
1104
1078
  console.log();
1105
1079
  });
1106
- // ── Trailing Stop ──
1107
- trade
1108
- .command("trailing-stop <symbol>")
1109
- .description("Client-side trailing stop that monitors price and closes position when price drops by X% from peak")
1110
- .requiredOption("--trail <pct>", "Trail percentage (e.g., 3 = close when price drops 3% from high)")
1111
- .option("--interval <sec>", "Check interval in seconds", "5")
1112
- .option("--activation <price>", "Only start trailing after price reaches this level")
1113
- .option("--background", "Run in background (tmux)")
1114
- .action(async (symbol, opts) => {
1115
- const sym = symbol.toUpperCase();
1116
- const trailPct = parseFloat(opts.trail);
1117
- const intervalSec = parseInt(opts.interval);
1118
- const activationPrice = opts.activation ? parseFloat(opts.activation) : undefined;
1119
- if (isNaN(trailPct) || trailPct <= 0)
1120
- errorAndExit("Trail percentage must be > 0");
1121
- const exchange = (await getAdapter()).name;
1122
- // --background → run via tmux
1123
- if (opts.background) {
1124
- const { startJob } = await import("../jobs.js");
1125
- const cliArgs = [
1126
- `-e`, exchange, sym,
1127
- `--trail`, opts.trail,
1128
- `--interval`, opts.interval,
1129
- ...(opts.activation ? [`--activation`, opts.activation] : []),
1130
- ];
1131
- const job = startJob({
1132
- strategy: "trailing-stop",
1133
- exchange,
1134
- params: { symbol: sym, trail: trailPct, interval: intervalSec, activation: activationPrice },
1135
- cliArgs,
1136
- });
1137
- if (isJson())
1138
- return printJson(jsonOk(job));
1139
- console.log(chalk.green(`\n Trailing stop started in background.`));
1140
- console.log(` ID: ${chalk.white.bold(job.id)}`);
1141
- console.log(` Trail: ${trailPct}%${activationPrice ? ` | Activation: $${activationPrice}` : ""}`);
1142
- console.log(` Logs: ${chalk.gray(`perp jobs logs ${job.id}`)}`);
1143
- console.log(` Stop: ${chalk.gray(`perp jobs stop ${job.id}`)}\n`);
1144
- return;
1145
- }
1146
- // Foreground: run trailing stop loop
1147
- const adapter = await getAdapter();
1148
- // Auto-detect position side
1149
- const positions = await adapter.getPositions();
1150
- const pos = positions.find(p => symbolMatch(p.symbol, sym));
1151
- if (!pos)
1152
- errorAndExit(`No open position for ${sym}. Open a position first.`);
1153
- const positionSide = pos.side; // "long" or "short"
1154
- const closeSide = positionSide === "long" ? "sell" : "buy";
1155
- const posSize = pos.size;
1156
- console.log(chalk.cyan(`\n Trailing Stop for ${sym} (${positionSide} ${posSize})`));
1157
- console.log(chalk.cyan(` Trail: ${trailPct}% | Interval: ${intervalSec}s${activationPrice ? ` | Activation: $${activationPrice}` : ""}`));
1158
- console.log(chalk.gray(` Press Ctrl+C to cancel.\n`));
1159
- let peakPrice = 0;
1160
- let activated = !activationPrice; // if no activation price, start immediately
1161
- let running = true;
1162
- const cleanup = () => { running = false; };
1163
- process.on("SIGINT", cleanup);
1164
- process.on("SIGTERM", cleanup);
1165
- try {
1166
- while (running) {
1167
- const markets = await adapter.getMarkets();
1168
- const market = markets.find(m => symbolMatch(m.symbol, sym));
1169
- if (!market) {
1170
- console.log(chalk.yellow(` Market data for ${sym} not found, retrying...`));
1171
- await new Promise(r => setTimeout(r, intervalSec * 1000));
1172
- continue;
1173
- }
1174
- const currentPrice = parseFloat(market.markPrice);
1175
- // Check activation
1176
- if (!activated && activationPrice) {
1177
- if (positionSide === "long" && currentPrice >= activationPrice) {
1178
- activated = true;
1179
- console.log(chalk.green(` Activated at $${currentPrice.toFixed(2)} (>= $${activationPrice})`));
1180
- }
1181
- else if (positionSide === "short" && currentPrice <= activationPrice) {
1182
- activated = true;
1183
- console.log(chalk.green(` Activated at $${currentPrice.toFixed(2)} (<= $${activationPrice})`));
1184
- }
1185
- else {
1186
- const ts = new Date().toLocaleTimeString();
1187
- console.log(chalk.gray(` ${ts} | $${currentPrice.toFixed(2)} | Waiting for activation ($${activationPrice})...`));
1188
- await new Promise(r => setTimeout(r, intervalSec * 1000));
1189
- continue;
1190
- }
1191
- }
1192
- // Update peak
1193
- if (positionSide === "long") {
1194
- if (currentPrice > peakPrice)
1195
- peakPrice = currentPrice;
1196
- const dropPct = ((peakPrice - currentPrice) / peakPrice) * 100;
1197
- const ts = new Date().toLocaleTimeString();
1198
- console.log(chalk.gray(` ${ts} | Price: $${currentPrice.toFixed(2)} | Peak: $${peakPrice.toFixed(2)} | Drop: ${dropPct.toFixed(2)}%`));
1199
- if (dropPct >= trailPct) {
1200
- console.log(chalk.red(`\n TRAILING STOP TRIGGERED! Price dropped ${dropPct.toFixed(2)}% from peak $${peakPrice.toFixed(2)}`));
1201
- console.log(chalk.red(` Closing ${positionSide} ${posSize} ${sym}...\n`));
1202
- const result = await adapter.marketOrder(sym, closeSide, posSize);
1203
- logExecution({
1204
- type: "market_order", exchange: adapter.name, symbol: sym,
1205
- side: closeSide, size: posSize, status: "success", dryRun: false,
1206
- meta: { action: "trailing-stop", trailPct, peakPrice, triggerPrice: currentPrice },
1207
- });
1208
- if (isJson())
1209
- return printJson(jsonOk({ triggered: true, peakPrice, triggerPrice: currentPrice, dropPct, result }));
1210
- console.log(chalk.green(` Position closed. Peak: $${peakPrice.toFixed(2)}, Exit: $${currentPrice.toFixed(2)}\n`));
1211
- return;
1212
- }
1213
- }
1214
- else {
1215
- // Short position: track lowest price, trigger when price rises
1216
- if (peakPrice === 0 || currentPrice < peakPrice)
1217
- peakPrice = currentPrice;
1218
- const risePct = ((currentPrice - peakPrice) / peakPrice) * 100;
1219
- const ts = new Date().toLocaleTimeString();
1220
- console.log(chalk.gray(` ${ts} | Price: $${currentPrice.toFixed(2)} | Trough: $${peakPrice.toFixed(2)} | Rise: ${risePct.toFixed(2)}%`));
1221
- if (risePct >= trailPct) {
1222
- console.log(chalk.red(`\n TRAILING STOP TRIGGERED! Price rose ${risePct.toFixed(2)}% from trough $${peakPrice.toFixed(2)}`));
1223
- console.log(chalk.red(` Closing ${positionSide} ${posSize} ${sym}...\n`));
1224
- const result = await adapter.marketOrder(sym, closeSide, posSize);
1225
- logExecution({
1226
- type: "market_order", exchange: adapter.name, symbol: sym,
1227
- side: closeSide, size: posSize, status: "success", dryRun: false,
1228
- meta: { action: "trailing-stop", trailPct, troughPrice: peakPrice, triggerPrice: currentPrice },
1229
- });
1230
- if (isJson())
1231
- return printJson(jsonOk({ triggered: true, troughPrice: peakPrice, triggerPrice: currentPrice, risePct, result }));
1232
- console.log(chalk.green(` Position closed. Trough: $${peakPrice.toFixed(2)}, Exit: $${currentPrice.toFixed(2)}\n`));
1233
- return;
1234
- }
1235
- }
1236
- await new Promise(r => setTimeout(r, intervalSec * 1000));
1237
- }
1238
- }
1239
- finally {
1240
- process.removeListener("SIGINT", cleanup);
1241
- process.removeListener("SIGTERM", cleanup);
1242
- }
1243
- if (isJson())
1244
- return printJson(jsonOk({ triggered: false, reason: "cancelled" }));
1245
- console.log(chalk.yellow(`\n Trailing stop cancelled.\n`));
1246
- });
1080
+ // grid/dca/trailing-stop moved to 'perp bot'
1247
1081
  // ── PnL Tracker ──
1248
1082
  trade
1249
1083
  .command("pnl-track")
@@ -1324,4 +1158,173 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
1324
1158
  }
1325
1159
  console.log(chalk.yellow(`\n PnL tracker stopped.\n`));
1326
1160
  });
1161
+ // ── Multi-leg orders ──────────────────────────────────────────────────
1162
+ if (getAdapterForExchange) {
1163
+ const tradeCmd = program.commands.find(c => c.name() === "trade");
1164
+ if (tradeCmd) {
1165
+ registerMultiAction(tradeCmd, getAdapterForExchange, isJson);
1166
+ }
1167
+ // Keep deprecated top-level 'multi' alias (hidden from help)
1168
+ registerMultiAction(program, getAdapterForExchange, isJson, true);
1169
+ }
1170
+ }
1171
+ function parseMultiLeg(spec) {
1172
+ const parts = spec.split(":");
1173
+ if (parts.length !== 4) {
1174
+ throw new Error(`Invalid leg format "${spec}" — expected exchange:symbol:side:size`);
1175
+ }
1176
+ const [exchange, symbol, side, size] = parts;
1177
+ // Normalize exchange aliases
1178
+ const exMap = {
1179
+ hl: "hyperliquid", pac: "pacifica", lit: "lighter",
1180
+ hyperliquid: "hyperliquid", pacifica: "pacifica", lighter: "lighter",
1181
+ };
1182
+ const normalizedExchange = exMap[exchange.toLowerCase()];
1183
+ if (!normalizedExchange) {
1184
+ throw new Error(`Unknown exchange "${exchange}" in leg spec`);
1185
+ }
1186
+ if (side !== "buy" && side !== "sell") {
1187
+ throw new Error(`Invalid side "${side}" — must be "buy" or "sell"`);
1188
+ }
1189
+ if (isNaN(Number(size)) || Number(size) <= 0) {
1190
+ throw new Error(`Invalid size "${size}" — must be a positive number`);
1191
+ }
1192
+ return { exchange: normalizedExchange, symbol: symbol.toUpperCase(), side, size };
1193
+ }
1194
+ function registerMultiAction(parent, getAdapterForExchange, isJson, deprecated = false) {
1195
+ const cmd = parent
1196
+ .command("multi <legs...>")
1197
+ .description(deprecated ? "Use 'perp trade multi'" : "Execute multi-leg orders (exchange:symbol:side:size)");
1198
+ if (deprecated)
1199
+ cmd._hidden = true;
1200
+ cmd
1201
+ .option("--smart", "Use smart order (IOC limit + fallback) for each leg")
1202
+ .option("--rollback", "Auto-rollback filled legs if any leg fails", true)
1203
+ .option("--no-rollback", "Disable auto-rollback")
1204
+ .option("--timeout <ms>", "Per-leg timeout in milliseconds", "30000")
1205
+ .action(async (legSpecs, opts) => {
1206
+ await withJsonErrors(isJson(), async () => {
1207
+ // Parse legs
1208
+ const legs = legSpecs.map(parseMultiLeg);
1209
+ if (legs.length < 2) {
1210
+ const err = "Multi-leg requires at least 2 legs";
1211
+ if (isJson()) {
1212
+ printJson(jsonError("INVALID_ARGS", err));
1213
+ return;
1214
+ }
1215
+ throw new Error(err);
1216
+ }
1217
+ if (!isJson()) {
1218
+ console.log(chalk.cyan.bold("Multi-Leg Order\n"));
1219
+ for (const l of legs) {
1220
+ const color = l.side === "buy" ? chalk.green : chalk.red;
1221
+ console.log(` ${l.exchange.padEnd(14)} ${color(l.side.toUpperCase())} ${l.symbol} x ${l.size}`);
1222
+ }
1223
+ console.log();
1224
+ }
1225
+ // Get adapters (deduplicated)
1226
+ const adapters = new Map();
1227
+ for (const l of legs) {
1228
+ if (!adapters.has(l.exchange)) {
1229
+ adapters.set(l.exchange, await getAdapterForExchange(l.exchange));
1230
+ }
1231
+ }
1232
+ // Execute all legs simultaneously
1233
+ const timeoutMs = parseInt(opts.timeout);
1234
+ const results = await Promise.allSettled(legs.map(async (leg) => {
1235
+ const adapter = adapters.get(leg.exchange);
1236
+ const orderPromise = opts.smart
1237
+ ? smartOrder(adapter, leg.symbol, leg.side, leg.size).then(r => r.result)
1238
+ : adapter.marketOrder(leg.symbol, leg.side, leg.size);
1239
+ // Apply timeout
1240
+ const result = await Promise.race([
1241
+ orderPromise,
1242
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Leg timeout")), timeoutMs)),
1243
+ ]);
1244
+ // Log execution
1245
+ logExecution({
1246
+ type: "multi_leg",
1247
+ exchange: leg.exchange,
1248
+ symbol: leg.symbol,
1249
+ side: leg.side,
1250
+ size: leg.size,
1251
+ status: "success",
1252
+ dryRun: false,
1253
+ meta: { result, smart: opts.smart },
1254
+ });
1255
+ return { leg, status: "filled", result };
1256
+ }));
1257
+ // Process results
1258
+ const legResults = results.map((r, i) => {
1259
+ if (r.status === "fulfilled")
1260
+ return r.value;
1261
+ return {
1262
+ leg: legs[i],
1263
+ status: "failed",
1264
+ error: r.reason instanceof Error ? r.reason.message : String(r.reason),
1265
+ };
1266
+ });
1267
+ const filled = legResults.filter(r => r.status === "filled");
1268
+ const failed = legResults.filter(r => r.status === "failed");
1269
+ // Rollback if partial fill and rollback enabled
1270
+ if (failed.length > 0 && filled.length > 0 && opts.rollback) {
1271
+ if (!isJson()) {
1272
+ console.log(chalk.yellow(`\n ${failed.length} leg(s) failed — rolling back ${filled.length} filled leg(s)...\n`));
1273
+ }
1274
+ for (const fr of filled) {
1275
+ const adapter = adapters.get(fr.leg.exchange);
1276
+ const reverseSide = fr.leg.side === "buy" ? "sell" : "buy";
1277
+ try {
1278
+ await adapter.marketOrder(fr.leg.symbol, reverseSide, fr.leg.size);
1279
+ fr.status = "rolled_back";
1280
+ logExecution({
1281
+ type: "multi_leg_rollback",
1282
+ exchange: fr.leg.exchange,
1283
+ symbol: fr.leg.symbol,
1284
+ side: reverseSide,
1285
+ size: fr.leg.size,
1286
+ status: "success",
1287
+ dryRun: false,
1288
+ });
1289
+ if (!isJson()) {
1290
+ console.log(chalk.gray(` Rolled back: ${fr.leg.exchange} ${reverseSide} ${fr.leg.symbol} x ${fr.leg.size}`));
1291
+ }
1292
+ }
1293
+ catch (err) {
1294
+ if (!isJson()) {
1295
+ console.log(chalk.red(` Rollback failed: ${fr.leg.exchange} ${fr.leg.symbol} — ${err instanceof Error ? err.message : err}`));
1296
+ }
1297
+ }
1298
+ }
1299
+ }
1300
+ if (isJson()) {
1301
+ printJson(jsonOk({
1302
+ legs: legResults.map(r => ({
1303
+ exchange: r.leg.exchange,
1304
+ symbol: r.leg.symbol,
1305
+ side: r.leg.side,
1306
+ size: r.leg.size,
1307
+ status: r.status,
1308
+ error: r.error,
1309
+ })),
1310
+ summary: {
1311
+ total: legs.length,
1312
+ filled: legResults.filter(r => r.status === "filled").length,
1313
+ failed: failed.length,
1314
+ rolledBack: legResults.filter(r => r.status === "rolled_back").length,
1315
+ },
1316
+ }));
1317
+ }
1318
+ else {
1319
+ console.log(chalk.cyan.bold("\nResults:\n"));
1320
+ for (const r of legResults) {
1321
+ const icon = r.status === "filled" ? chalk.green("OK") :
1322
+ r.status === "rolled_back" ? chalk.yellow("ROLLBACK") :
1323
+ chalk.red("FAIL");
1324
+ console.log(` ${icon} ${r.leg.exchange.padEnd(14)} ${r.leg.side} ${r.leg.symbol} x ${r.leg.size}${r.error ? ` — ${r.error}` : ""}`);
1325
+ }
1326
+ console.log(`\n Total: ${legs.length} Filled: ${filled.length} Failed: ${failed.length} Rolled back: ${legResults.filter(r => r.status === "rolled_back").length}`);
1327
+ }
1328
+ });
1329
+ });
1327
1330
  }
@@ -11,4 +11,8 @@ export declare function getWalletSetupStatus(): {
11
11
  };
12
12
  /** Exported so config.ts can resolve the active wallet key */
13
13
  export declare function getActiveWalletKey(exchange: string): string | null;
14
+ /** Get wallet key by name. Returns null if wallet doesn't exist. */
15
+ export declare function getWalletKeyByName(name: string): string | null;
16
+ /** Get all wallet names. */
17
+ export declare function getWalletNames(): string[];
14
18
  export declare function registerWalletCommands(program: Command, isJson: () => boolean): void;
@@ -42,6 +42,16 @@ export function getActiveWalletKey(exchange) {
42
42
  return null;
43
43
  return store.wallets[name]?.privateKey ?? null;
44
44
  }
45
+ /** Get wallet key by name. Returns null if wallet doesn't exist. */
46
+ export function getWalletKeyByName(name) {
47
+ const store = loadStore();
48
+ return store.wallets[name]?.privateKey ?? null;
49
+ }
50
+ /** Get all wallet names. */
51
+ export function getWalletNames() {
52
+ const store = loadStore();
53
+ return Object.keys(store.wallets);
54
+ }
45
55
  async function getSolanaBalances(address, isTestnet) {
46
56
  const { Connection, PublicKey } = await import("@solana/web3.js");
47
57
  const rpcUrl = isTestnet
@@ -389,7 +399,7 @@ export function registerWalletCommands(program, isJson) {
389
399
  const { LighterAdapter } = await import("../exchanges/lighter.js");
390
400
  const adapter = new LighterAdapter(normalized);
391
401
  await adapter.init();
392
- const apiKeyIndex = 2;
402
+ const apiKeyIndex = 4;
393
403
  const { privateKey: apiKey } = await adapter.setupApiKey(apiKeyIndex);
394
404
  setEnvVar("LIGHTER_API_KEY", apiKey);
395
405
  setEnvVar("LIGHTER_ACCOUNT_INDEX", String(adapter.accountIndex));
package/dist/config.d.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { Keypair } from "@solana/web3.js";
2
2
  export type Exchange = "pacifica" | "hyperliquid" | "lighter";
3
- export declare function loadPrivateKey(exchange: Exchange, pkOverride?: string): Promise<string>;
3
+ /**
4
+ * Try to load a private key without throwing if none is found.
5
+ * Returns null if no key is configured.
6
+ */
7
+ export declare function tryLoadPrivateKey(exchange: Exchange, pkOverride?: string, walletName?: string): Promise<string | null>;
8
+ export declare function loadPrivateKey(exchange: Exchange, pkOverride?: string, walletName?: string): Promise<string>;
4
9
  export declare function parseSolanaKeypair(input: string): Keypair;
5
10
  export declare function isEvmPrivateKey(input: string): boolean;