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
package/dist/index.js CHANGED
@@ -7,39 +7,35 @@ config({ path: resolve(process.env.HOME || "~", ".perp", ".env") });
7
7
  config();
8
8
  import { Command } from "commander";
9
9
  import chalk from "chalk";
10
- import { loadPrivateKey, parseSolanaKeypair } from "./config.js";
10
+ import { Keypair } from "@solana/web3.js";
11
+ import { tryLoadPrivateKey, parseSolanaKeypair } from "./config.js";
11
12
  import { PacificaAdapter } from "./exchanges/pacifica.js";
12
13
  import { HyperliquidAdapter } from "./exchanges/hyperliquid.js";
13
14
  import { registerMarketCommands } from "./commands/market.js";
14
15
  import { registerAccountCommands } from "./commands/account.js";
15
16
  import { registerTradeCommands } from "./commands/trade.js";
16
17
  import { registerManageCommands } from "./commands/manage.js";
17
- import { registerStreamCommands } from "./commands/stream.js";
18
+ // stream commands removed — WS feeds still used by dashboard/event-stream internally
18
19
  import { registerArbCommands } from "./commands/arb.js";
19
20
  import { registerWalletCommands } from "./commands/wallet.js";
20
21
  import { registerBridgeCommands } from "./commands/bridge.js";
21
- import { registerDepositCommands } from "./commands/deposit.js";
22
- import { registerAlertCommands } from "./commands/alert.js";
22
+ // deposit + withdraw merged into funds
23
+ import { registerFundsCommands } from "./commands/funds.js";
24
+ // alert commands removed
23
25
  import { registerArbAutoCommands } from "./commands/arb-auto.js";
24
- import { registerArbManageCommands } from "./commands/arb-manage.js";
25
- import { registerGapCommands } from "./commands/gap.js";
26
+ import { registerArbManageCommands } from "./commands/arb/index.js";
26
27
  import { registerAgentCommands } from "./commands/agent.js";
27
- import { registerWithdrawCommands } from "./commands/withdraw.js";
28
28
  import { registerRebalanceCommands } from "./commands/rebalance.js";
29
29
  import { registerBotCommands } from "./commands/bot.js";
30
- import { registerHealthCommands } from "./commands/health.js";
31
- import { registerPortfolioCommands } from "./commands/portfolio.js";
32
30
  import { registerRiskCommands } from "./commands/risk.js";
33
31
  import { registerHistoryCommands } from "./commands/history.js";
34
- import { registerAnalyticsCommands } from "./commands/analytics.js";
35
32
  import { registerSettingsCommands } from "./commands/settings.js";
36
- import { registerDexCommands } from "./commands/dex.js";
33
+ // dex commands merged into market (hip3) — use --dex flag for markets/balance
37
34
  import { registerPlanCommands } from "./commands/plan.js";
38
- import { registerFundingCommands } from "./commands/funding.js";
35
+ // funding merged into arb.ts
39
36
  import { registerBacktestCommands } from "./commands/backtest.js";
40
37
  import { registerDashboardCommands } from "./commands/dashboard.js";
41
38
  import { registerInitCommand, EXCHANGE_ENV_MAP, validateKey } from "./commands/init.js";
42
- import { registerEnvCommands } from "./commands/env.js";
43
39
  import { loadSettings, saveSettings } from "./settings.js";
44
40
  import { setSharedApiNetwork } from "./shared-api.js";
45
41
  const _require = createRequire(import.meta.url);
@@ -59,6 +55,7 @@ program
59
55
  .option("--fields <fields>", "Comma-separated fields to include in JSON output (e.g. totalEquity,positions)")
60
56
  .option("--ndjson", "Output newline-delimited JSON (one object per line for streaming)")
61
57
  .option("--dry-run", "Simulate trades without executing (log as simulated)")
58
+ .option("-w, --wallet <name>", "Use a specific wallet by name (from 'perp wallet list')")
62
59
  .option("--dex <name>", "HIP-3 deployed perp dex name (Hyperliquid only)")
63
60
  .configureOutput({
64
61
  writeErr: (str) => {
@@ -80,71 +77,83 @@ let _adapter = null;
80
77
  let _pacificaAdapter = null;
81
78
  let _hlAdapter = null;
82
79
  let _lighterAdapter = null;
80
+ /** Map short aliases to canonical exchange names. Full names pass through. */
81
+ function resolveExchangeAlias(name) {
82
+ const aliases = {
83
+ hl: "hyperliquid",
84
+ lt: "lighter",
85
+ pac: "pacifica",
86
+ };
87
+ return aliases[name.toLowerCase()] ?? name.toLowerCase();
88
+ }
83
89
  function getExchange() {
84
- return program.opts().exchange;
90
+ return resolveExchangeAlias(program.opts().exchange);
85
91
  }
86
92
  async function getAdapter() {
87
- if (_adapter)
88
- return _adapter;
89
93
  const opts = program.opts();
90
- const exchange = opts.exchange;
94
+ const walletName = opts.wallet;
95
+ // Skip cache when --wallet is specified (different wallet = different account)
96
+ if (!walletName && _adapter)
97
+ return _adapter;
98
+ const exchange = resolveExchangeAlias(opts.exchange);
91
99
  const network = opts.network;
92
100
  const isTestnet = network === "testnet";
101
+ // Try to load key — null means no key configured (read-only mode)
102
+ const pk = await tryLoadPrivateKey(exchange, opts.privateKey, walletName);
93
103
  switch (exchange) {
94
104
  case "pacifica": {
95
- const pk = await loadPrivateKey("pacifica", opts.privateKey);
96
- const keypair = parseSolanaKeypair(pk);
105
+ const keypair = pk ? parseSolanaKeypair(pk) : Keypair.generate();
97
106
  const pacNetwork = (isTestnet ? "testnet" : "mainnet");
98
107
  const settings = loadSettings();
99
108
  const builderCode = process.env.PACIFICA_BUILDER_CODE || settings.referralCodes.pacifica || "PERPCLI";
100
- _pacificaAdapter = new PacificaAdapter(keypair, pacNetwork, builderCode);
109
+ _pacificaAdapter = new PacificaAdapter(keypair, pacNetwork, builderCode, !!pk);
101
110
  _adapter = _pacificaAdapter;
102
111
  break;
103
112
  }
104
113
  case "hyperliquid": {
105
- const pk = await loadPrivateKey("hyperliquid", opts.privateKey);
106
- _hlAdapter = new HyperliquidAdapter(pk, isTestnet);
114
+ _hlAdapter = new HyperliquidAdapter(pk ?? undefined, isTestnet);
107
115
  if (opts.dex)
108
116
  _hlAdapter.setDex(opts.dex);
109
117
  await _hlAdapter.init();
110
- const hlSettings = loadSettings();
111
- if (hlSettings.referrals && !hlSettings.referralApplied.hyperliquid) {
112
- const hlRef = process.env.HL_REFERRAL_CODE || hlSettings.referralCodes.hyperliquid;
113
- if (hlRef) {
114
- _hlAdapter.autoSetReferrer(hlRef).then(() => {
115
- const s = loadSettings();
116
- s.referralApplied.hyperliquid = true;
117
- saveSettings(s);
118
- }).catch(() => {
119
- // Already referred or API error — mark as done either way
120
- const s = loadSettings();
121
- s.referralApplied.hyperliquid = true;
122
- saveSettings(s);
123
- });
118
+ if (pk) {
119
+ const hlSettings = loadSettings();
120
+ if (hlSettings.referrals && !hlSettings.referralApplied.hyperliquid) {
121
+ const hlRef = process.env.HL_REFERRAL_CODE || hlSettings.referralCodes.hyperliquid;
122
+ if (hlRef) {
123
+ _hlAdapter.autoSetReferrer(hlRef).then(() => {
124
+ const s = loadSettings();
125
+ s.referralApplied.hyperliquid = true;
126
+ saveSettings(s);
127
+ }).catch(() => {
128
+ const s = loadSettings();
129
+ s.referralApplied.hyperliquid = true;
130
+ saveSettings(s);
131
+ });
132
+ }
124
133
  }
125
134
  }
126
135
  _adapter = _hlAdapter;
127
136
  break;
128
137
  }
129
138
  case "lighter": {
130
- const pk = await loadPrivateKey("lighter", opts.privateKey);
131
139
  const { LighterAdapter } = await import("./exchanges/lighter.js");
132
- _lighterAdapter = new LighterAdapter(pk, isTestnet);
140
+ _lighterAdapter = new LighterAdapter(pk ?? "", isTestnet);
133
141
  await _lighterAdapter.init();
134
- const ltSettings = loadSettings();
135
- if (ltSettings.referrals && !ltSettings.referralApplied.lighter) {
136
- const ltRef = process.env.LIGHTER_REFERRAL_CODE || ltSettings.referralCodes.lighter;
137
- if (ltRef) {
138
- _lighterAdapter.useReferralCode(ltRef).then(() => {
139
- const s = loadSettings();
140
- s.referralApplied.lighter = true;
141
- saveSettings(s);
142
- }).catch(() => {
143
- // Already referred or API error — mark as done either way
144
- const s = loadSettings();
145
- s.referralApplied.lighter = true;
146
- saveSettings(s);
147
- });
142
+ if (pk) {
143
+ const ltSettings = loadSettings();
144
+ if (ltSettings.referrals && !ltSettings.referralApplied.lighter) {
145
+ const ltRef = process.env.LIGHTER_REFERRAL_CODE || ltSettings.referralCodes.lighter;
146
+ if (ltRef) {
147
+ _lighterAdapter.useReferralCode(ltRef).then(() => {
148
+ const s = loadSettings();
149
+ s.referralApplied.lighter = true;
150
+ saveSettings(s);
151
+ }).catch(() => {
152
+ const s = loadSettings();
153
+ s.referralApplied.lighter = true;
154
+ saveSettings(s);
155
+ });
156
+ }
148
157
  }
149
158
  }
150
159
  _adapter = _lighterAdapter;
@@ -162,7 +171,7 @@ function getAdapterSync() {
162
171
  return _adapter;
163
172
  }
164
173
  function isJson() {
165
- return !!program.opts().json;
174
+ return !!program.opts().json || process.argv.includes("--ndjson");
166
175
  }
167
176
  function isDryRun() {
168
177
  return !!program.opts().dryRun;
@@ -180,30 +189,32 @@ function getHLAdapter() {
180
189
  }
181
190
  // Register command groups with async adapter getter
182
191
  registerMarketCommands(program, getAdapter, isJson);
183
- registerAccountCommands(program, getAdapter, isJson);
184
- registerTradeCommands(program, getAdapter, isJson, isDryRun);
192
+ registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange);
193
+ registerTradeCommands(program, getAdapter, isJson, isDryRun, getAdapterForExchange);
185
194
  registerManageCommands(program, getAdapter, isJson, getPacificaAdapter);
186
- registerStreamCommands(program, () => program.opts().network, getExchange, getAdapter);
187
- registerArbCommands(program, isJson);
195
+ // stream commands removed
196
+ registerArbCommands(program, isJson, getAdapterForExchange);
188
197
  registerWalletCommands(program, isJson);
189
198
  registerBridgeCommands(program, isJson);
190
- registerDepositCommands(program, getAdapter, isJson, () => program.opts().network);
191
- registerAlertCommands(program, isJson, getAdapterForExchange);
199
+ registerFundsCommands(program, getAdapter, isJson, () => program.opts().network);
200
+ // alert commands removed
192
201
  // Helper to get adapter for a specific exchange (used by arb-auto)
193
- async function getAdapterForExchange(exchange) {
202
+ async function getAdapterForExchange(rawExchange) {
203
+ const exchange = resolveExchangeAlias(rawExchange);
194
204
  const opts = program.opts();
195
205
  const network = opts.network;
196
206
  const isTestnet = network === "testnet";
207
+ const walletName = opts.wallet;
208
+ const pk = await tryLoadPrivateKey(exchange, opts.privateKey, walletName);
197
209
  switch (exchange) {
198
210
  case "pacifica": {
199
211
  if (_pacificaAdapter)
200
212
  return _pacificaAdapter;
201
- const pk = await loadPrivateKey("pacifica", opts.privateKey);
202
- const keypair = parseSolanaKeypair(pk);
213
+ const keypair = pk ? parseSolanaKeypair(pk) : Keypair.generate();
203
214
  const pacNetwork = (isTestnet ? "testnet" : "mainnet");
204
215
  const s1 = loadSettings();
205
216
  const builderCode = process.env.PACIFICA_BUILDER_CODE || s1.referralCodes.pacifica || "PERPCLI";
206
- _pacificaAdapter = new PacificaAdapter(keypair, pacNetwork, builderCode);
217
+ _pacificaAdapter = new PacificaAdapter(keypair, pacNetwork, builderCode, !!pk);
207
218
  if (!_adapter)
208
219
  _adapter = _pacificaAdapter;
209
220
  return _pacificaAdapter;
@@ -211,24 +222,25 @@ async function getAdapterForExchange(exchange) {
211
222
  case "hyperliquid": {
212
223
  if (_hlAdapter)
213
224
  return _hlAdapter;
214
- const pk = await loadPrivateKey("hyperliquid", opts.privateKey);
215
- _hlAdapter = new HyperliquidAdapter(pk, isTestnet);
225
+ _hlAdapter = new HyperliquidAdapter(pk ?? undefined, isTestnet);
216
226
  if (opts.dex)
217
227
  _hlAdapter.setDex(opts.dex);
218
228
  await _hlAdapter.init();
219
- const s2 = loadSettings();
220
- if (s2.referrals && !s2.referralApplied.hyperliquid) {
221
- const hlRef = process.env.HL_REFERRAL_CODE || s2.referralCodes.hyperliquid;
222
- if (hlRef) {
223
- _hlAdapter.autoSetReferrer(hlRef).then(() => {
224
- const s = loadSettings();
225
- s.referralApplied.hyperliquid = true;
226
- saveSettings(s);
227
- }).catch(() => {
228
- const s = loadSettings();
229
- s.referralApplied.hyperliquid = true;
230
- saveSettings(s);
231
- });
229
+ if (pk) {
230
+ const s2 = loadSettings();
231
+ if (s2.referrals && !s2.referralApplied.hyperliquid) {
232
+ const hlRef = process.env.HL_REFERRAL_CODE || s2.referralCodes.hyperliquid;
233
+ if (hlRef) {
234
+ _hlAdapter.autoSetReferrer(hlRef).then(() => {
235
+ const s = loadSettings();
236
+ s.referralApplied.hyperliquid = true;
237
+ saveSettings(s);
238
+ }).catch(() => {
239
+ const s = loadSettings();
240
+ s.referralApplied.hyperliquid = true;
241
+ saveSettings(s);
242
+ });
243
+ }
232
244
  }
233
245
  }
234
246
  if (!_adapter)
@@ -238,23 +250,24 @@ async function getAdapterForExchange(exchange) {
238
250
  case "lighter": {
239
251
  if (_lighterAdapter)
240
252
  return _lighterAdapter;
241
- const pk = await loadPrivateKey("lighter", opts.privateKey);
242
253
  const { LighterAdapter } = await import("./exchanges/lighter.js");
243
- _lighterAdapter = new LighterAdapter(pk, isTestnet);
254
+ _lighterAdapter = new LighterAdapter(pk ?? "", isTestnet);
244
255
  await _lighterAdapter.init();
245
- const s3 = loadSettings();
246
- if (s3.referrals && !s3.referralApplied.lighter) {
247
- const ltRef = process.env.LIGHTER_REFERRAL_CODE || s3.referralCodes.lighter;
248
- if (ltRef) {
249
- _lighterAdapter.useReferralCode(ltRef).then(() => {
250
- const s = loadSettings();
251
- s.referralApplied.lighter = true;
252
- saveSettings(s);
253
- }).catch(() => {
254
- const s = loadSettings();
255
- s.referralApplied.lighter = true;
256
- saveSettings(s);
257
- });
256
+ if (pk) {
257
+ const s3 = loadSettings();
258
+ if (s3.referrals && !s3.referralApplied.lighter) {
259
+ const ltRef = process.env.LIGHTER_REFERRAL_CODE || s3.referralCodes.lighter;
260
+ if (ltRef) {
261
+ _lighterAdapter.useReferralCode(ltRef).then(() => {
262
+ const s = loadSettings();
263
+ s.referralApplied.lighter = true;
264
+ saveSettings(s);
265
+ }).catch(() => {
266
+ const s = loadSettings();
267
+ s.referralApplied.lighter = true;
268
+ saveSettings(s);
269
+ });
270
+ }
258
271
  }
259
272
  }
260
273
  if (!_adapter)
@@ -271,8 +284,9 @@ async function getHLAdapterForDex(dex) {
271
284
  if (_dexAdapters.has(dex))
272
285
  return _dexAdapters.get(dex);
273
286
  const opts = program.opts();
274
- const pk = await loadPrivateKey("hyperliquid", opts.privateKey);
275
- const adapter = new HyperliquidAdapter(pk, opts.network === "testnet");
287
+ const walletName = opts.wallet;
288
+ const pk = await tryLoadPrivateKey("hyperliquid", opts.privateKey, walletName);
289
+ const adapter = new HyperliquidAdapter(pk ?? undefined, opts.network === "testnet");
276
290
  if (dex !== "hl")
277
291
  adapter.setDex(dex);
278
292
  await adapter.init();
@@ -281,92 +295,43 @@ async function getHLAdapterForDex(dex) {
281
295
  }
282
296
  registerArbAutoCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex);
283
297
  registerArbManageCommands(program, getAdapterForExchange, isJson);
284
- registerGapCommands(program, isJson);
285
298
  registerAgentCommands(program, getAdapter, isJson);
286
- registerWithdrawCommands(program, getAdapter, isJson);
299
+ // withdraw merged into funds
287
300
  registerRebalanceCommands(program, getAdapterForExchange, isJson);
288
301
  // Jobs & strategies
289
302
  import { registerJobsCommands } from "./commands/jobs.js";
290
- import { registerRunCommands } from "./commands/run.js";
291
303
  registerJobsCommands(program, isJson);
292
- registerRunCommands(program, getAdapter, getAdapterForExchange, isJson);
293
304
  registerBotCommands(program, getAdapter, getAdapterForExchange, isJson);
294
305
  // Agent-friendly commands
295
- registerHealthCommands(program, isJson);
296
- registerPortfolioCommands(program, getAdapterForExchange, isJson);
297
306
  registerRiskCommands(program, getAdapterForExchange, isJson);
298
- registerHistoryCommands(program, isJson);
299
- registerAnalyticsCommands(program, getAdapterForExchange, isJson);
307
+ registerHistoryCommands(program, isJson, getAdapterForExchange);
300
308
  registerSettingsCommands(program, isJson, getAdapterForExchange);
301
- registerDexCommands(program, getAdapter, isJson);
309
+ // dex commands removed — use 'market hip3' + --dex flag
302
310
  registerPlanCommands(program, getAdapter, isJson);
303
- registerFundingCommands(program, isJson);
311
+ // funding merged into arb — registerFundingCommands removed
304
312
  registerBacktestCommands(program, isJson);
305
313
  registerDashboardCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex);
306
314
  registerInitCommand(program);
307
- registerEnvCommands(program, isJson);
308
- // Agent discovery: perp api-spec — returns full CLI spec as JSON
309
- program
315
+ // Agent discovery: perp api-spec — deprecated, use 'perp agent schema'
316
+ const apiSpecCmd = program
310
317
  .command("api-spec")
311
- .description("Return full CLI command spec as JSON (for agent discovery)")
318
+ .description("Use 'perp agent schema'")
312
319
  .action(async () => {
313
320
  const { jsonOk, printJson } = await import("./utils.js");
314
321
  const { getCliSpec } = await import("./cli-spec.js");
315
322
  printJson(jsonOk(getCliSpec(program)));
316
323
  });
317
- // Status command
318
- program
319
- .command("status")
320
- .description("Quick overview: account + positions + open orders")
321
- .action(async () => {
322
- const adapter = await getAdapter();
323
- const json = isJson();
324
- try {
325
- const [balance, positions, orders] = await Promise.all([
326
- adapter.getBalance(),
327
- adapter.getPositions(),
328
- adapter.getOpenOrders(),
329
- ]);
330
- if (json) {
331
- const { jsonOk, printJson } = await import("./utils.js");
332
- return printJson(jsonOk({ exchange: adapter.name, balance, positions, orders }));
333
- }
334
- console.log(chalk.cyan.bold(`\n ${adapter.name.toUpperCase()} Account Status\n`));
335
- console.log(` Equity: $${Number(balance.equity).toFixed(2)}`);
336
- console.log(` Available: $${Number(balance.available).toFixed(2)}`);
337
- console.log(` Margin Used: $${Number(balance.marginUsed).toFixed(2)}`);
338
- console.log(` Positions: ${positions.length}`);
339
- console.log(` Open Orders: ${orders.length}`);
340
- if (positions.length > 0) {
341
- console.log(chalk.cyan.bold("\n Positions:"));
342
- positions.forEach((p) => {
343
- const color = p.side === "long" ? chalk.green : chalk.red;
344
- const pnlNum = Number(p.unrealizedPnl);
345
- const pnlColor = pnlNum >= 0 ? chalk.green : chalk.red;
346
- console.log(` ${color(p.side.toUpperCase().padEnd(5))} ${chalk.white(p.symbol.padEnd(12))} ${p.size.padEnd(10)} entry: $${Number(p.entryPrice).toFixed(2)} pnl: ${pnlColor(pnlNum >= 0 ? "+" : "")}$${pnlNum.toFixed(2)}`);
347
- });
348
- }
349
- if (orders.length > 0) {
350
- console.log(chalk.cyan.bold("\n Open Orders:"));
351
- orders.forEach((o) => {
352
- const color = o.side === "buy" ? chalk.green : chalk.red;
353
- console.log(` ${color(o.side.toUpperCase().padEnd(4))} ${chalk.white(o.symbol.padEnd(12))} ${o.type.padEnd(8)} $${Number(o.price).toFixed(2)} x ${o.size}`);
354
- });
355
- }
356
- console.log();
357
- }
358
- catch (err) {
359
- const msg = err instanceof Error ? err.message : String(err);
360
- if (isJson()) {
361
- const { jsonError } = await import("./utils.js");
362
- console.log(JSON.stringify(jsonError("COMMAND_ERROR", msg)));
363
- }
364
- else {
365
- console.error(chalk.red(`Error: ${msg}`));
366
- }
367
- process.exit(1);
324
+ apiSpecCmd._hidden = true;
325
+ // Deprecated: perp status → use 'perp portfolio' or 'perp account'
326
+ const statusCmd = program.command("status").description("Use 'perp portfolio'")
327
+ .option("--health", "Check connectivity").action(async (opts) => {
328
+ if (opts.health) {
329
+ const { runHealthCheck } = await import("./commands/risk.js");
330
+ return runHealthCheck(isJson);
368
331
  }
332
+ console.log("Use 'perp portfolio' or 'perp account' instead.");
369
333
  });
334
+ statusCmd._hidden = true;
370
335
  // Switch shared API URLs if --network testnet is used
371
336
  program.hook("preAction", () => {
372
337
  const network = program.opts().network;
@@ -375,7 +340,7 @@ program.hook("preAction", () => {
375
340
  });
376
341
  // Smart landing page: `perp` with no subcommand
377
342
  const rawArgs = process.argv.slice(2);
378
- const hasSubcommand = rawArgs.some((a) => !a.startsWith("-") && !["pacifica", "hyperliquid", "lighter", "mainnet", "testnet"].includes(a));
343
+ const hasSubcommand = rawArgs.some((a) => !a.startsWith("-") && !["pacifica", "hyperliquid", "lighter", "hl", "lt", "pac", "mainnet", "testnet"].includes(a));
379
344
  if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawArgs.includes("--help") && !rawArgs.includes("-V") && !rawArgs.includes("--version"))) {
380
345
  // No subcommand — show smart landing instead of help dump
381
346
  (async () => {
@@ -1,4 +1,5 @@
1
1
  import type { ExchangeAdapter } from "./exchanges/interface.js";
2
+ import type { SpotAdapter } from "./exchanges/spot-interface.js";
2
3
  export interface LiquidityCheck {
3
4
  /** Max executable size (base) within slippage tolerance */
4
5
  maxSize: number;
@@ -32,3 +33,14 @@ export declare function checkArbLiquidity(longAdapter: ExchangeAdapter, shortAda
32
33
  longSlippage: number;
33
34
  shortSlippage: number;
34
35
  }>;
36
+ /**
37
+ * Check liquidity for a spot+perp arb pair.
38
+ * Spot side uses asks (for buy) or bids (for sell).
39
+ * Perp side uses bids (for short entry) or asks (for long entry).
40
+ */
41
+ export declare function checkSpotPerpLiquidity(spotAdapter: SpotAdapter, perpAdapter: ExchangeAdapter, spotSymbol: string, perpSymbol: string, sizeUsd: number, maxSlippagePct?: number, log?: (msg: string) => void): Promise<{
42
+ viable: boolean;
43
+ adjustedSizeUsd: number;
44
+ spotSlippage: number;
45
+ perpSlippage: number;
46
+ }>;
package/dist/liquidity.js CHANGED
@@ -98,3 +98,46 @@ export async function checkArbLiquidity(longAdapter, shortAdapter, symbol, sizeU
98
98
  return { viable: false, adjustedSizeUsd: 0, longSlippage: 0, shortSlippage: 0 };
99
99
  }
100
100
  }
101
+ /**
102
+ * Check liquidity for a spot+perp arb pair.
103
+ * Spot side uses asks (for buy) or bids (for sell).
104
+ * Perp side uses bids (for short entry) or asks (for long entry).
105
+ */
106
+ export async function checkSpotPerpLiquidity(spotAdapter, perpAdapter, spotSymbol, perpSymbol, sizeUsd, maxSlippagePct = 0.5, log) {
107
+ try {
108
+ const [spotOB, perpOB] = await Promise.all([
109
+ spotAdapter.getSpotOrderbook(spotSymbol),
110
+ perpAdapter.getOrderbook(perpSymbol),
111
+ ]);
112
+ // Spot buy = consume asks; Perp short = consume bids
113
+ const spotCheck = computeExecutableSize(spotOB.asks, sizeUsd, maxSlippagePct);
114
+ const perpCheck = computeExecutableSize(perpOB.bids, sizeUsd, maxSlippagePct);
115
+ // Cross-market price gap check (spot vs perp)
116
+ if (spotOB.asks.length > 0 && perpOB.bids.length > 0) {
117
+ const bestSpotAsk = Number(spotOB.asks[0][0]);
118
+ const bestPerpBid = Number(perpOB.bids[0][0]);
119
+ const gapPct = Math.abs(bestSpotAsk - bestPerpBid) / Math.min(bestSpotAsk, bestPerpBid) * 100;
120
+ if (gapPct > 2) {
121
+ log?.(`[LIQ] spot-perp ${spotSymbol}: price gap ${gapPct.toFixed(2)}% too wide`);
122
+ return { viable: false, adjustedSizeUsd: 0, spotSlippage: spotCheck.slippagePct, perpSlippage: perpCheck.slippagePct };
123
+ }
124
+ }
125
+ const executableUsd = Math.min(spotCheck.maxSize * spotCheck.avgFillPrice, perpCheck.maxSize * perpCheck.avgFillPrice);
126
+ const minViable = sizeUsd * 0.2;
127
+ if (executableUsd < minViable) {
128
+ log?.(`[LIQ] spot-perp ${spotSymbol}: only $${executableUsd.toFixed(0)} executable (need min $${minViable.toFixed(0)})`);
129
+ return { viable: false, adjustedSizeUsd: 0, spotSlippage: spotCheck.slippagePct, perpSlippage: perpCheck.slippagePct };
130
+ }
131
+ const adjustedSizeUsd = Math.min(sizeUsd, executableUsd);
132
+ return {
133
+ viable: true,
134
+ adjustedSizeUsd,
135
+ spotSlippage: spotCheck.slippagePct,
136
+ perpSlippage: perpCheck.slippagePct,
137
+ };
138
+ }
139
+ catch {
140
+ log?.(`[LIQ] spot-perp ${spotSymbol}: orderbook fetch failed`);
141
+ return { viable: false, adjustedSizeUsd: 0, spotSlippage: 0, perpSlippage: 0 };
142
+ }
143
+ }