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
package/README.md CHANGED
@@ -33,6 +33,8 @@ perp wallet show
33
33
 
34
34
  Same EVM key works for both Hyperliquid and Lighter.
35
35
 
36
+ > **Lighter API Key Index:** Indexes 0–3 are reserved by Lighter's frontend (web/mobile). perp-cli defaults to index `4`. Override with `LIGHTER_API_KEY_INDEX` env var or `--key-index` flag on `manage setup-api-key`. Valid range: 4–254.
37
+
36
38
  ## Core Commands
37
39
 
38
40
  ```bash
@@ -5,7 +5,18 @@ import type { ExchangeAdapter } from "../exchanges/interface.js";
5
5
  *
6
6
  * @returns size string that works for both exchanges, or null if not viable
7
7
  */
8
- export declare function computeMatchedSize(sizeUsd: number, price: number, longExchange: string, shortExchange: string): {
8
+ export declare function computeMatchedSize(sizeUsd: number, price: number, longExchange: string, shortExchange: string, opts?: {
9
+ longSizeDecimals?: number;
10
+ shortSizeDecimals?: number;
11
+ }): {
12
+ size: string;
13
+ notional: number;
14
+ } | null;
15
+ /**
16
+ * Compute matched size for spot-perp arb.
17
+ * Uses spot exchange's size decimals for the spot leg.
18
+ */
19
+ export declare function computeSpotPerpMatchedSize(sizeUsd: number, price: number, spotExchange: string, perpExchange: string, spotSizeDecimals?: number): {
9
20
  size: string;
10
21
  notional: number;
11
22
  } | null;
@@ -4,12 +4,12 @@
4
4
  *
5
5
  * @returns size string that works for both exchanges, or null if not viable
6
6
  */
7
- export function computeMatchedSize(sizeUsd, price, longExchange, shortExchange) {
7
+ export function computeMatchedSize(sizeUsd, price, longExchange, shortExchange, opts) {
8
8
  if (price <= 0)
9
9
  return null;
10
10
  const rawSize = sizeUsd / price;
11
- // Size decimals per exchange (conservative defaults)
12
- const szDecimals = Math.min(getSizeDecimals(longExchange), getSizeDecimals(shortExchange));
11
+ // Use explicitly provided decimals if available, else conservative defaults
12
+ const szDecimals = Math.min(opts?.longSizeDecimals ?? getSizeDecimals(longExchange), opts?.shortSizeDecimals ?? getSizeDecimals(shortExchange));
13
13
  // Round DOWN to the least precise exchange's decimals
14
14
  const factor = Math.pow(10, szDecimals);
15
15
  const roundedSize = Math.floor(rawSize * factor) / factor;
@@ -30,12 +30,15 @@ export function computeMatchedSize(sizeUsd, price, longExchange, shortExchange)
30
30
  }
31
31
  return { size: roundedSize.toFixed(szDecimals), notional };
32
32
  }
33
- /** Size decimal precision by exchange */
33
+ /** Size decimal precision by exchange (conservative fallbacks — prefer explicit decimals via opts) */
34
34
  function getSizeDecimals(exchange) {
35
35
  switch (exchange.toLowerCase()) {
36
- case "hyperliquid": return 1; // Most HL perps use 1 decimal
36
+ case "hyperliquid": return 2; // HL perps range 0-5; 2 is safe middle ground
37
37
  case "lighter": return 2;
38
38
  case "pacifica": return 4;
39
+ // Spot exchanges
40
+ case "spot:hyperliquid": return 2;
41
+ case "spot:lighter": return 2;
39
42
  default: return 2;
40
43
  }
41
44
  }
@@ -45,9 +48,39 @@ function getMinNotional(exchange) {
45
48
  case "hyperliquid": return 10;
46
49
  case "lighter": return 10;
47
50
  case "pacifica": return 1;
51
+ case "spot:hyperliquid": return 10;
52
+ case "spot:lighter": return 10;
48
53
  default: return 10;
49
54
  }
50
55
  }
56
+ /**
57
+ * Compute matched size for spot-perp arb.
58
+ * Uses spot exchange's size decimals for the spot leg.
59
+ */
60
+ export function computeSpotPerpMatchedSize(sizeUsd, price, spotExchange, perpExchange, spotSizeDecimals) {
61
+ if (price <= 0)
62
+ return null;
63
+ // If explicit spot decimals provided, use them; otherwise derive from exchange
64
+ const spotDec = spotSizeDecimals ?? getSizeDecimals(`spot:${spotExchange}`);
65
+ const perpDec = getSizeDecimals(perpExchange);
66
+ const szDecimals = Math.min(spotDec, perpDec);
67
+ const rawSize = sizeUsd / price;
68
+ const factor = Math.pow(10, szDecimals);
69
+ const roundedSize = Math.floor(rawSize * factor) / factor;
70
+ if (roundedSize <= 0)
71
+ return null;
72
+ const notional = roundedSize * price;
73
+ const minNotional = Math.max(getMinNotional(`spot:${spotExchange}`), getMinNotional(perpExchange));
74
+ if (notional < minNotional) {
75
+ const roundedUp = Math.ceil(rawSize * factor) / factor;
76
+ const notionalUp = roundedUp * price;
77
+ if (notionalUp <= sizeUsd * 1.2) {
78
+ return { size: roundedUp.toFixed(szDecimals), notional: notionalUp };
79
+ }
80
+ return null;
81
+ }
82
+ return { size: roundedSize.toFixed(szDecimals), notional };
83
+ }
51
84
  /**
52
85
  * After both legs are submitted, verify actual fills match.
53
86
  * If there's a size mismatch, place a correction order on the larger side.
@@ -13,6 +13,12 @@ export interface ArbPositionState {
13
13
  entryShortPrice: number;
14
14
  accumulatedFunding: number;
15
15
  lastCheckTime: string;
16
+ /** "perp-perp" (default) or "spot-perp" */
17
+ mode?: "perp-perp" | "spot-perp";
18
+ /** Spot leg exchange name (for spot-perp mode) */
19
+ spotExchange?: string;
20
+ /** Spot symbol, e.g. "ETH/USDC" (for spot-perp mode) */
21
+ spotSymbol?: string;
16
22
  }
17
23
  export interface ArbDaemonState {
18
24
  version: 1;
@@ -2,6 +2,8 @@
2
2
  * Arb utilities: settlement timing strategy, basis risk monitoring, and webhook notifications.
3
3
  */
4
4
  import type { ExchangeAdapter } from "../exchanges/interface.js";
5
+ /** Settlement schedules per exchange (UTC hours when settlement occurs) */
6
+ export declare const SETTLEMENT_SCHEDULES: Record<string, number[]>;
5
7
  export type SettleStrategy = "block" | "aggressive" | "off";
6
8
  /**
7
9
  * Get the most recent settlement time for an exchange.
package/dist/arb/utils.js CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  // ── Settlement Timing Strategy ──
5
5
  /** Settlement schedules per exchange (UTC hours when settlement occurs) */
6
- const SETTLEMENT_SCHEDULES = {
6
+ export const SETTLEMENT_SCHEDULES = {
7
7
  hyperliquid: Array.from({ length: 24 }, (_, i) => i), // every hour
8
8
  pacifica: Array.from({ length: 24 }, (_, i) => i), // every hour
9
9
  lighter: Array.from({ length: 24 }, (_, i) => i), // every hour
@@ -1,7 +1,3 @@
1
- /**
2
- * Generates a structured CLI spec from Commander's program tree.
3
- * Used by `perp api-spec` so agents can discover all commands programmatically.
4
- */
5
1
  import type { Command } from "commander";
6
2
  interface ArgSpec {
7
3
  name: string;
package/dist/cli-spec.js CHANGED
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Generates a structured CLI spec from Commander's program tree.
3
+ * Used by `perp api-spec` so agents can discover all commands programmatically.
4
+ */
5
+ import { createRequire } from "node:module";
6
+ const require = createRequire(import.meta.url);
7
+ const { version: pkgVersion } = require("../package.json");
1
8
  function extractCommand(cmd) {
2
9
  const args = cmd._args ?? [];
3
10
  const options = cmd.options.map((opt) => ({
@@ -27,7 +34,7 @@ export function getCliSpec(program) {
27
34
  }));
28
35
  return {
29
36
  name: "perp",
30
- version: "0.1.0",
37
+ version: pkgVersion,
31
38
  description: "Multi-DEX Perpetual Futures CLI (Pacifica, Hyperliquid, Lighter)",
32
39
  globalOptions,
33
40
  commands,
@@ -1,6 +1,9 @@
1
+ import { createRequire } from "node:module";
1
2
  import { printJson, jsonOk } from "../utils.js";
2
3
  import { ERROR_CODES } from "../errors.js";
3
4
  import chalk from "chalk";
5
+ const _require = createRequire(import.meta.url);
6
+ const _pkg = _require("../../package.json");
4
7
  /** Infer parameter type from commander flags/description */
5
8
  function inferType(flags, desc) {
6
9
  if (flags.includes("[boolean]") || !flags.includes("<") && !flags.includes("["))
@@ -72,7 +75,7 @@ export function registerAgentCommands(program, getAdapter, isJson) {
72
75
  }
73
76
  const envelope = {
74
77
  schemaVersion: "2.0",
75
- cliVersion: "0.1.0",
78
+ cliVersion: _pkg.version,
76
79
  generatedAt: new Date().toISOString(),
77
80
  exchanges: ["pacifica", "hyperliquid", "lighter"],
78
81
  errorCodes: errorCodeDocs,
@@ -93,7 +96,7 @@ export function registerAgentCommands(program, getAdapter, isJson) {
93
96
  }
94
97
  const envelope = {
95
98
  schemaVersion: "2.0",
96
- cliVersion: "0.1.0",
99
+ cliVersion: _pkg.version,
97
100
  generatedAt: new Date().toISOString(),
98
101
  exchanges: ["pacifica", "hyperliquid", "lighter"],
99
102
  errorCodes: errorCodeDocs,
@@ -110,7 +113,7 @@ export function registerAgentCommands(program, getAdapter, isJson) {
110
113
  .action(() => {
111
114
  printJson(jsonOk({
112
115
  name: "perp-cli",
113
- version: "0.1.0",
116
+ version: _pkg.version,
114
117
  description: "Multi-DEX Perpetual Futures CLI (Pacifica, Hyperliquid, Lighter) with HIP-3 deployed dex support",
115
118
  exchanges: ["pacifica", "hyperliquid", "lighter"],
116
119
  capabilities: [
@@ -142,14 +145,14 @@ export function registerAgentCommands(program, getAdapter, isJson) {
142
145
  category: "trading",
143
146
  commands: [
144
147
  "perp trade check <symbol> <side> <size> --json",
145
- "perp trade market <symbol> <buy|sell> <size> --json --client-id <id>",
148
+ "perp trade market <symbol> <buy|sell> <size> --json [--smart] --client-id <id>",
146
149
  "perp trade limit <symbol> <buy|sell> <price> <size> --json --client-id <id>",
147
150
  "perp trade cancel <symbol> <orderId> --json",
148
151
  "perp trade cancel-all --json",
149
- "perp trade close <symbol> --json",
150
- "perp trade close-all --json",
151
- "perp trade flatten --json",
152
- "perp trade reduce <symbol> <percent> --json",
152
+ "perp trade close <symbol> --json [--smart]",
153
+ "perp trade close-all --json [--smart]",
154
+ "perp trade flatten --json [--smart]",
155
+ "perp trade reduce <symbol> <percent> --json [--smart]",
153
156
  "perp trade stop <symbol> <side> <stopPrice> <size> --json",
154
157
  "perp trade tpsl <symbol> <side> --tp <price> --sl <price> --json",
155
158
  "perp trade twap <symbol> <side> <size> <duration> --json",
@@ -276,7 +279,7 @@ export function registerAgentCommands(program, getAdapter, isJson) {
276
279
  .action(async () => {
277
280
  const results = {
278
281
  timestamp: new Date().toISOString(),
279
- cli_version: "0.1.0",
282
+ cli_version: _pkg.version,
280
283
  };
281
284
  // Check exchange APIs
282
285
  const { pingPacifica, pingHyperliquid } = await import("../shared-api.js");
@@ -4,12 +4,14 @@ import { readExecutionLog, logExecution } from "../../execution-log.js";
4
4
  import { computeAnnualSpread } from "../../funding.js";
5
5
  import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates, } from "../../shared-api.js";
6
6
  import { computeBasisRisk } from "../../arb-utils.js";
7
+ import { smartOrder } from "../../smart-order.js";
8
+ import { removePosition as persistRemovePosition } from "../../arb-state.js";
7
9
  import { fetchAllBalances, computeRebalancePlan } from "../../rebalance.js";
8
10
  import { EXCHANGE_TO_CHAIN, getBestQuote } from "../../bridge-engine.js";
9
11
  import { computeEnhancedStats } from "../../arb-history-stats.js";
10
12
  // ── Helpers ──
13
+ import { DEFAULT_TAKER_FEE as TAKER_FEE } from "../../constants.js";
11
14
  const EXCHANGES = ["hyperliquid", "lighter", "pacifica"];
12
- const TAKER_FEE = 0.00035; // ~0.035% typical taker fee
13
15
  function formatDuration(ms) {
14
16
  const hours = Math.floor(ms / (1000 * 60 * 60));
15
17
  const days = Math.floor(hours / 24);
@@ -293,6 +295,7 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
293
295
  .description("Manually close an arb position on both exchanges")
294
296
  .option("--dry-run", "Show what would happen without executing")
295
297
  .option("--pair <pair>", "Specify arb pair as longExchange:shortExchange (e.g. pacifica:hyperliquid)")
298
+ .option("--smart", "Smart execution: IOC limit at best bid/ask + 1 tick (reduces slippage)")
296
299
  .action(async (symbol, opts) => {
297
300
  const sym = symbol.toUpperCase();
298
301
  const dryRun = !!opts.dryRun || process.argv.includes("--dry-run");
@@ -436,10 +439,15 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
436
439
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
437
440
  try {
438
441
  const adapter = await getAdapterForExchange(exchange);
439
- await adapter.marketOrder(rawSymbol, side, String(size));
442
+ if (opts.smart) {
443
+ await smartOrder(adapter, rawSymbol, side, String(size), { reduceOnly: true });
444
+ }
445
+ else {
446
+ await adapter.marketOrder(rawSymbol, side, String(size));
447
+ }
440
448
  results.push({ exchange, action: `${side} (close ${label})`, status: "success", retries: attempt });
441
449
  if (!isJson())
442
- console.log(chalk.green(` Closed ${label} on ${exchange}: ${side.toUpperCase()} ${size} ${sym}${attempt > 0 ? ` (retry ${attempt})` : ""}`));
450
+ console.log(chalk.green(` Closed ${label} on ${exchange}: ${side.toUpperCase()} ${size} ${sym}${opts.smart ? " (smart)" : ""}${attempt > 0 ? ` (retry ${attempt})` : ""}`));
443
451
  return;
444
452
  }
445
453
  catch (err) {
@@ -488,6 +496,13 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
488
496
  console.log(chalk.yellow(`\n Warning: Position may not be fully closed on: ${notClosed.map(v => v.exchange).join(", ")}`));
489
497
  }
490
498
  }
499
+ // Remove from persisted daemon state (so daemon doesn't try to re-close)
500
+ if (allSuccess) {
501
+ try {
502
+ persistRemovePosition(sym);
503
+ }
504
+ catch { /* state may not exist */ }
505
+ }
491
506
  // Log to execution log
492
507
  const arbPairId = `${sym}:${longPos.exchange}:${shortPos.exchange}`;
493
508
  logExecution({