perp-cli 0.4.8 → 0.4.10

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 (45) hide show
  1. package/README.md +44 -5
  2. package/dist/arb/reconcile.d.ts +28 -0
  3. package/dist/arb/reconcile.js +162 -0
  4. package/dist/commands/account.d.ts +1 -1
  5. package/dist/commands/account.js +366 -62
  6. package/dist/commands/agent.js +9 -5
  7. package/dist/commands/arb/index.js +243 -49
  8. package/dist/commands/arb-auto.js +67 -19
  9. package/dist/commands/arb.d.ts +2 -1
  10. package/dist/commands/arb.js +535 -131
  11. package/dist/commands/bot.js +207 -0
  12. package/dist/commands/funds.d.ts +4 -0
  13. package/dist/commands/funds.js +741 -0
  14. package/dist/commands/history.d.ts +2 -1
  15. package/dist/commands/history.js +508 -5
  16. package/dist/commands/manage.js +5 -3
  17. package/dist/commands/market.js +34 -1
  18. package/dist/commands/pnl.js +1 -1
  19. package/dist/commands/risk.d.ts +2 -0
  20. package/dist/commands/risk.js +56 -0
  21. package/dist/commands/settings.js +147 -0
  22. package/dist/commands/trade.d.ts +1 -1
  23. package/dist/commands/trade.js +241 -259
  24. package/dist/commands/wallet.d.ts +4 -0
  25. package/dist/commands/wallet.js +10 -0
  26. package/dist/config.d.ts +2 -2
  27. package/dist/config.js +11 -3
  28. package/dist/exchanges/hyperliquid-spot.d.ts +1 -0
  29. package/dist/exchanges/hyperliquid-spot.js +13 -21
  30. package/dist/exchanges/lighter-spot.js +13 -15
  31. package/dist/exchanges/lighter.js +82 -21
  32. package/dist/exchanges/pacifica.js +12 -0
  33. package/dist/execution-log.d.ts +1 -1
  34. package/dist/index.js +39 -123
  35. package/dist/mcp-server.js +43 -96
  36. package/dist/strategies/split-order.d.ts +49 -0
  37. package/dist/strategies/split-order.js +138 -0
  38. package/package.json +1 -1
  39. package/skills/perp-cli/SKILL.md +13 -26
  40. package/skills/perp-cli/references/agent-operations.md +19 -9
  41. package/skills/perp-cli/references/commands.md +38 -31
  42. package/skills/perp-cli/references/strategies.md +26 -16
  43. package/skills/perp-cli/scripts/funding-analysis.sh +2 -2
  44. package/skills/perp-cli/scripts/preflight.sh +1 -1
  45. package/skills/perp-cli/scripts/spot-perp-scan.sh +1 -1
@@ -27,6 +27,7 @@ export class HyperliquidSpotAdapter {
27
27
  _spotAssetReverse = new Map(); // spotIndex → "ETH"
28
28
  _spotDecimals = new Map();
29
29
  _initialized = false;
30
+ _cachedMetaCtx = null;
30
31
  constructor(hlAdapter) {
31
32
  this._hl = hlAdapter;
32
33
  }
@@ -39,9 +40,12 @@ export class HyperliquidSpotAdapter {
39
40
  }
40
41
  async _loadSpotMeta() {
41
42
  try {
42
- const meta = await this._infoPost({ type: "spotMeta" });
43
- if (!meta?.universe)
43
+ // Single API call — spotMetaAndAssetCtxs includes all data from spotMeta
44
+ const metaCtx = await this._infoPost({ type: "spotMetaAndAssetCtxs" });
45
+ if (!metaCtx?.[0]?.universe)
44
46
  return;
47
+ this._cachedMetaCtx = metaCtx;
48
+ const meta = metaCtx[0];
45
49
  // Token index → name map
46
50
  const tokenNames = new Map();
47
51
  for (const t of meta.tokens ?? []) {
@@ -54,29 +58,15 @@ export class HyperliquidSpotAdapter {
54
58
  if (!baseToken)
55
59
  continue;
56
60
  const key = baseToken.toUpperCase();
57
- // Prefer /USDC pairs (quote token = USDC). Skip non-USDC pairs if we already have one.
58
61
  const isUsdcPair = u.tokens[1] === usdcTokenIndex;
59
62
  if (this._spotAssetMap.has(key) && !isUsdcPair)
60
63
  continue;
61
64
  this._spotAssetMap.set(key, u.index);
62
65
  this._spotAssetReverse.set(u.index, key);
63
- }
64
- // Get spot decimals from metaAndAssetCtxs
65
- const metaCtx = await this._infoPost({ type: "spotMetaAndAssetCtxs" });
66
- if (metaCtx?.[0]?.universe) {
67
- for (const u of metaCtx[0].universe) {
68
- const baseToken = tokenNames.get(u.tokens[0]) ?? "";
69
- if (!baseToken)
70
- continue;
71
- const key = baseToken.toUpperCase();
72
- const isUsdcPair = u.tokens[1] === usdcTokenIndex;
73
- if (this._spotDecimals.has(key) && !isUsdcPair)
74
- continue;
75
- this._spotDecimals.set(key, {
76
- size: u.szDecimals ?? 2,
77
- price: 6, // HL spot prices use up to 6 significant digits
78
- });
79
- }
66
+ this._spotDecimals.set(key, {
67
+ size: u.szDecimals ?? 2,
68
+ price: 6,
69
+ });
80
70
  }
81
71
  }
82
72
  catch (e) {
@@ -86,7 +76,9 @@ export class HyperliquidSpotAdapter {
86
76
  async getSpotMarkets() {
87
77
  await this.init();
88
78
  try {
89
- const metaCtx = await this._infoPost({ type: "spotMetaAndAssetCtxs" });
79
+ // Reuse cached metaCtx from init, or fetch fresh if cache expired
80
+ const metaCtx = this._cachedMetaCtx
81
+ ?? await this._infoPost({ type: "spotMetaAndAssetCtxs" });
90
82
  if (!metaCtx?.[0]?.universe)
91
83
  return [];
92
84
  const tokenNames = new Map();
@@ -83,28 +83,26 @@ export class LighterSpotAdapter {
83
83
  }
84
84
  async getSpotMarkets() {
85
85
  await this.init();
86
- const markets = [];
87
- for (const [symbol, marketId] of this._spotMarketMap) {
86
+ const entries = Array.from(this._spotMarketMap.entries());
87
+ // Fetch all orderbooks in parallel (instead of sequential per-market)
88
+ const obResults = await Promise.allSettled(entries.map(([symbol]) => this.getSpotOrderbook(symbol).then(ob => {
89
+ if (ob.bids.length > 0 && ob.asks.length > 0) {
90
+ return String((Number(ob.bids[0][0]) + Number(ob.asks[0][0])) / 2);
91
+ }
92
+ return "0";
93
+ })));
94
+ return entries.map(([symbol], i) => {
88
95
  const parts = symbol.split("/");
89
96
  const base = parts[0] ?? "";
90
97
  const quote = parts[1] ?? "USDC";
91
98
  const dec = this._spotDecimals.get(symbol) ?? { size: 4, price: 2 };
92
- // Get mark price from orderbook mid
93
- let markPrice = "0";
94
- try {
95
- const ob = await this.getSpotOrderbook(symbol);
96
- if (ob.bids.length > 0 && ob.asks.length > 0) {
97
- markPrice = String((Number(ob.bids[0][0]) + Number(ob.asks[0][0])) / 2);
98
- }
99
- }
100
- catch { /* skip */ }
101
- markets.push({
99
+ const markPrice = obResults[i].status === "fulfilled" ? obResults[i].value : "0";
100
+ return {
102
101
  symbol, baseToken: base, quoteToken: quote,
103
102
  markPrice, volume24h: "0",
104
103
  sizeDecimals: dec.size, priceDecimals: dec.price,
105
- });
106
- }
107
- return markets;
104
+ };
105
+ });
108
106
  }
109
107
  async getSpotOrderbook(symbol) {
110
108
  await this.init();
@@ -73,12 +73,28 @@ export class LighterAdapter {
73
73
  await this._refreshMarketMap();
74
74
  return;
75
75
  }
76
- const res = await fetch(`${this._baseUrl}/api/v1/account?by=l1_address&value=${this._address}`);
77
- const json = await res.json();
78
- if (json.accounts && json.accounts.length > 0) {
79
- this._accountIndex = this._accountIndexInit >= 0
80
- ? this._accountIndexInit
81
- : json.accounts[0].account_index;
76
+ // Retry account lookup on 429 (rate limit) — critical for _accountIndex
77
+ for (let attempt = 0; attempt < 3; attempt++) {
78
+ try {
79
+ const res = await fetch(`${this._baseUrl}/api/v1/account?by=l1_address&value=${this._address}`);
80
+ if (res.status === 429 && attempt < 2) {
81
+ await new Promise(r => setTimeout(r, 1000 * (attempt + 1)));
82
+ continue;
83
+ }
84
+ const json = await res.json();
85
+ if (json.accounts && json.accounts.length > 0) {
86
+ this._accountIndex = this._accountIndexInit >= 0
87
+ ? this._accountIndexInit
88
+ : json.accounts[0].account_index;
89
+ }
90
+ break;
91
+ }
92
+ catch {
93
+ if (attempt < 2) {
94
+ await new Promise(r => setTimeout(r, 1000));
95
+ continue;
96
+ }
97
+ }
82
98
  }
83
99
  // Auto-generate API key if we have PK but no API key and account exists
84
100
  let signerReady = false;
@@ -286,6 +302,8 @@ export class LighterAdapter {
286
302
  }
287
303
  }
288
304
  async getBalance() {
305
+ if (!this._address)
306
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
289
307
  const acct = await this.fetchAccount();
290
308
  if (!acct)
291
309
  return { equity: "0", available: "0", marginUsed: "0", unrealizedPnl: "0" };
@@ -301,6 +319,8 @@ export class LighterAdapter {
301
319
  };
302
320
  }
303
321
  async getPositions() {
322
+ if (!this._address)
323
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
304
324
  const acct = await this.fetchAccount();
305
325
  if (!acct)
306
326
  return [];
@@ -336,6 +356,8 @@ export class LighterAdapter {
336
356
  return this.restGet(path, params);
337
357
  }
338
358
  async getOpenOrders() {
359
+ if (!this._address)
360
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
339
361
  if (this._accountIndex < 0 || this._readOnly)
340
362
  return [];
341
363
  try {
@@ -344,13 +366,26 @@ export class LighterAdapter {
344
366
  const totalOrders = Number(acct?.total_order_count ?? 0);
345
367
  if (totalOrders === 0)
346
368
  return [];
347
- // Query each market in parallel (accountActiveOrders requires auth + market_id)
369
+ // Only query markets that have open orders (avoids 156-market fan-out)
370
+ const activeMarketIds = new Set();
371
+ for (const p of acct?.positions ?? []) {
372
+ if (Number(p.open_order_count ?? 0) > 0 || Number(p.pending_order_count ?? 0) > 0) {
373
+ activeMarketIds.add(Number(p.market_id));
374
+ }
375
+ }
376
+ if (activeMarketIds.size === 0)
377
+ return [];
378
+ const reverseMap = new Map();
379
+ for (const [sym, id] of this._marketMap)
380
+ reverseMap.set(id, sym);
348
381
  const allOrders = [];
349
- const entries = Array.from(this._marketMap.entries());
350
- const results = await Promise.allSettled(entries.map(([sym, marketId]) => this.restGetAuth("/accountActiveOrders", {
351
- account_index: String(this._accountIndex),
352
- market_id: String(marketId),
353
- }).then(res => ({ sym, orders: (res.orders ?? []) }))));
382
+ const results = await Promise.allSettled(Array.from(activeMarketIds).map((marketId) => {
383
+ const sym = reverseMap.get(marketId) ?? `Market-${marketId}`;
384
+ return this.restGetAuth("/accountActiveOrders", {
385
+ account_index: String(this._accountIndex),
386
+ market_id: String(marketId),
387
+ }).then(res => ({ sym, orders: (res.orders ?? []) }));
388
+ }));
354
389
  for (const r of results) {
355
390
  if (r.status !== "fulfilled" || !r.value.orders.length)
356
391
  continue;
@@ -575,6 +610,8 @@ export class LighterAdapter {
575
610
  }));
576
611
  }
577
612
  async getOrderHistory(limit = 30) {
613
+ if (!this._address)
614
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
578
615
  const raw = await this._getOrderHistoryRaw();
579
616
  const orders = (raw.orders ?? []);
580
617
  return orders.slice(0, limit).map((o) => ({
@@ -589,6 +626,8 @@ export class LighterAdapter {
589
626
  }));
590
627
  }
591
628
  async getTradeHistory(limit = 30) {
629
+ if (!this._address)
630
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
592
631
  const raw = await this._getTradeHistoryRaw(limit);
593
632
  const trades = (raw.trades ?? []);
594
633
  return trades.slice(0, limit).map((t) => ({
@@ -601,6 +640,8 @@ export class LighterAdapter {
601
640
  }));
602
641
  }
603
642
  async getFundingPayments(limit = 30) {
643
+ if (!this._address)
644
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
604
645
  const raw = await this._getPositionFundingRaw();
605
646
  const items = (raw.funding ?? []);
606
647
  return items.slice(0, limit).map((f) => ({
@@ -704,15 +745,35 @@ export class LighterAdapter {
704
745
  return { funding: [] };
705
746
  if (this._marketMap.size === 0)
706
747
  await this._refreshMarketMap();
748
+ // Only query markets where the account has/had positions (avoids 156-market fan-out)
749
+ const acct = await this.fetchAccount();
750
+ const activeMarketIds = new Set();
751
+ if (acct) {
752
+ for (const p of acct.positions ?? []) {
753
+ const pos = Number(p.position || 0);
754
+ const rpnl = Number(p.realized_pnl || 0);
755
+ if (pos !== 0 || rpnl !== 0) {
756
+ activeMarketIds.add(Number(p.market_id));
757
+ }
758
+ }
759
+ }
760
+ if (activeMarketIds.size === 0)
761
+ return { funding: [] };
762
+ // Resolve market_id → symbol from marketMap
763
+ const reverseMap = new Map();
764
+ for (const [sym, id] of this._marketMap)
765
+ reverseMap.set(id, sym);
707
766
  const allFunding = [];
708
- const entries = Array.from(this._marketMap.entries());
709
- const results = await Promise.allSettled(entries.map(([sym, marketId]) => this.restGetAuth("/positionFunding", {
710
- account_index: String(this._accountIndex),
711
- market_id: String(marketId),
712
- }).then((res) => {
713
- const items = (res.funding ?? res.data ?? []);
714
- return items.map((f) => ({ ...f, symbol: f.symbol ?? sym }));
715
- })));
767
+ const results = await Promise.allSettled(Array.from(activeMarketIds).map((marketId) => {
768
+ const sym = reverseMap.get(marketId) ?? `Market-${marketId}`;
769
+ return this.restGetAuth("/positionFunding", {
770
+ account_index: String(this._accountIndex),
771
+ market_id: String(marketId),
772
+ }).then((res) => {
773
+ const items = (res.funding ?? res.data ?? []);
774
+ return items.map((f) => ({ ...f, symbol: f.symbol ?? sym }));
775
+ });
776
+ }));
716
777
  for (const r of results) {
717
778
  if (r.status === "fulfilled")
718
779
  allFunding.push(...r.value);
@@ -872,7 +933,7 @@ export class LighterAdapter {
872
933
  return res.json();
873
934
  }
874
935
  ensureSigner() {
875
- if (this._readOnly) {
936
+ if (this._readOnly || this._accountIndex < 0 || !this._signer) {
876
937
  throw new Error("This command requires a Lighter API key. Run `perp -e lighter manage setup-api-key` first, " +
877
938
  "then set LIGHTER_API_KEY in your .env");
878
939
  }
@@ -77,6 +77,8 @@ export class PacificaAdapter {
77
77
  };
78
78
  }
79
79
  async getBalance() {
80
+ if (!this._hasRealKey)
81
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
80
82
  const [info, positions] = await Promise.all([
81
83
  this.client.getAccount(this.account),
82
84
  this._getPositions(),
@@ -92,6 +94,8 @@ export class PacificaAdapter {
92
94
  };
93
95
  }
94
96
  async getPositions() {
97
+ if (!this._hasRealKey)
98
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
95
99
  const [positions, prices] = await Promise.all([
96
100
  this._getPositions(),
97
101
  this._getPrices(),
@@ -123,6 +127,8 @@ export class PacificaAdapter {
123
127
  });
124
128
  }
125
129
  async getOpenOrders() {
130
+ if (!this._hasRealKey)
131
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
126
132
  const orders = await this.client.getOrders(this.account);
127
133
  return orders.map((o) => {
128
134
  const raw = o;
@@ -220,6 +226,8 @@ export class PacificaAdapter {
220
226
  }));
221
227
  }
222
228
  async getOrderHistory(limit = 30) {
229
+ if (!this._hasRealKey)
230
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
223
231
  const result = await this.client.getOrderHistory(this.account);
224
232
  const data = (result.data ?? result.orders ?? []);
225
233
  if (!Array.isArray(data))
@@ -236,6 +244,8 @@ export class PacificaAdapter {
236
244
  }));
237
245
  }
238
246
  async getTradeHistory(limit = 30) {
247
+ if (!this._hasRealKey)
248
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
239
249
  const raw = await this.client.getTradeHistory(this.account);
240
250
  const trades = (raw.data ?? raw);
241
251
  if (!Array.isArray(trades))
@@ -250,6 +260,8 @@ export class PacificaAdapter {
250
260
  }));
251
261
  }
252
262
  async getFundingPayments(limit = 30) {
263
+ if (!this._hasRealKey)
264
+ throw new Error("No private key configured — account data unavailable. Run: perp init");
253
265
  const raw = await this.client.getFundingAccountHistory(this.account);
254
266
  const history = (raw.data ?? raw);
255
267
  if (!Array.isArray(history))
@@ -1,7 +1,7 @@
1
1
  export interface ExecutionRecord {
2
2
  id: string;
3
3
  timestamp: string;
4
- type: "market_order" | "limit_order" | "cancel" | "stop_order" | "edit_order" | "rebalance" | "arb_entry" | "arb_close" | "bridge" | "multi_leg" | "multi_leg_rollback";
4
+ type: "market_order" | "limit_order" | "cancel" | "stop_order" | "edit_order" | "rebalance" | "arb_entry" | "arb_close" | "bridge" | "multi_leg" | "multi_leg_rollback" | "split_order";
5
5
  exchange: string;
6
6
  symbol: string;
7
7
  side: string;
package/dist/index.js CHANGED
@@ -15,34 +15,27 @@ import { registerMarketCommands } from "./commands/market.js";
15
15
  import { registerAccountCommands } from "./commands/account.js";
16
16
  import { registerTradeCommands } from "./commands/trade.js";
17
17
  import { registerManageCommands } from "./commands/manage.js";
18
- import { registerStreamCommands } from "./commands/stream.js";
18
+ // stream commands removed — WS feeds still used by dashboard/event-stream internally
19
19
  import { registerArbCommands } from "./commands/arb.js";
20
20
  import { registerWalletCommands } from "./commands/wallet.js";
21
21
  import { registerBridgeCommands } from "./commands/bridge.js";
22
- import { registerDepositCommands } from "./commands/deposit.js";
23
- import { registerAlertCommands } from "./commands/alert.js";
22
+ // deposit + withdraw merged into funds
23
+ import { registerFundsCommands } from "./commands/funds.js";
24
+ // alert commands removed
24
25
  import { registerArbAutoCommands } from "./commands/arb-auto.js";
25
- import { registerArbManageCommands } from "./commands/arb-manage.js";
26
- import { registerGapCommands } from "./commands/gap.js";
26
+ import { registerArbManageCommands } from "./commands/arb/index.js";
27
27
  import { registerAgentCommands } from "./commands/agent.js";
28
- import { registerWithdrawCommands } from "./commands/withdraw.js";
29
28
  import { registerRebalanceCommands } from "./commands/rebalance.js";
30
29
  import { registerBotCommands } from "./commands/bot.js";
31
- import { registerHealthCommands } from "./commands/health.js";
32
- import { registerPortfolioCommands } from "./commands/portfolio.js";
33
30
  import { registerRiskCommands } from "./commands/risk.js";
34
31
  import { registerHistoryCommands } from "./commands/history.js";
35
- import { registerAnalyticsCommands } from "./commands/analytics.js";
36
- import { registerPnlCommands } from "./commands/pnl.js";
37
- import { registerMultilegCommands } from "./commands/multileg.js";
38
32
  import { registerSettingsCommands } from "./commands/settings.js";
39
- import { registerDexCommands } from "./commands/dex.js";
33
+ // dex commands merged into market (hip3) — use --dex flag for markets/balance
40
34
  import { registerPlanCommands } from "./commands/plan.js";
41
- import { registerFundingCommands } from "./commands/funding.js";
35
+ // funding merged into arb.ts
42
36
  import { registerBacktestCommands } from "./commands/backtest.js";
43
37
  import { registerDashboardCommands } from "./commands/dashboard.js";
44
38
  import { registerInitCommand, EXCHANGE_ENV_MAP, validateKey } from "./commands/init.js";
45
- import { registerEnvCommands } from "./commands/env.js";
46
39
  import { loadSettings, saveSettings } from "./settings.js";
47
40
  import { setSharedApiNetwork } from "./shared-api.js";
48
41
  const _require = createRequire(import.meta.url);
@@ -62,6 +55,7 @@ program
62
55
  .option("--fields <fields>", "Comma-separated fields to include in JSON output (e.g. totalEquity,positions)")
63
56
  .option("--ndjson", "Output newline-delimited JSON (one object per line for streaming)")
64
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')")
65
59
  .option("--dex <name>", "HIP-3 deployed perp dex name (Hyperliquid only)")
66
60
  .configureOutput({
67
61
  writeErr: (str) => {
@@ -96,14 +90,16 @@ function getExchange() {
96
90
  return resolveExchangeAlias(program.opts().exchange);
97
91
  }
98
92
  async function getAdapter() {
99
- if (_adapter)
100
- return _adapter;
101
93
  const opts = program.opts();
94
+ const walletName = opts.wallet;
95
+ // Skip cache when --wallet is specified (different wallet = different account)
96
+ if (!walletName && _adapter)
97
+ return _adapter;
102
98
  const exchange = resolveExchangeAlias(opts.exchange);
103
99
  const network = opts.network;
104
100
  const isTestnet = network === "testnet";
105
101
  // Try to load key — null means no key configured (read-only mode)
106
- const pk = await tryLoadPrivateKey(exchange, opts.privateKey);
102
+ const pk = await tryLoadPrivateKey(exchange, opts.privateKey, walletName);
107
103
  switch (exchange) {
108
104
  case "pacifica": {
109
105
  const keypair = pk ? parseSolanaKeypair(pk) : Keypair.generate();
@@ -193,22 +189,23 @@ function getHLAdapter() {
193
189
  }
194
190
  // Register command groups with async adapter getter
195
191
  registerMarketCommands(program, getAdapter, isJson);
196
- registerAccountCommands(program, getAdapter, isJson);
197
- registerTradeCommands(program, getAdapter, isJson, isDryRun);
192
+ registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange);
193
+ registerTradeCommands(program, getAdapter, isJson, isDryRun, getAdapterForExchange);
198
194
  registerManageCommands(program, getAdapter, isJson, getPacificaAdapter);
199
- registerStreamCommands(program, () => program.opts().network, getExchange, getAdapter);
200
- registerArbCommands(program, isJson);
195
+ // stream commands removed
196
+ registerArbCommands(program, isJson, getAdapterForExchange);
201
197
  registerWalletCommands(program, isJson);
202
198
  registerBridgeCommands(program, isJson);
203
- registerDepositCommands(program, getAdapter, isJson, () => program.opts().network);
204
- registerAlertCommands(program, isJson, getAdapterForExchange);
199
+ registerFundsCommands(program, getAdapter, isJson, () => program.opts().network);
200
+ // alert commands removed
205
201
  // Helper to get adapter for a specific exchange (used by arb-auto)
206
202
  async function getAdapterForExchange(rawExchange) {
207
203
  const exchange = resolveExchangeAlias(rawExchange);
208
204
  const opts = program.opts();
209
205
  const network = opts.network;
210
206
  const isTestnet = network === "testnet";
211
- const pk = await tryLoadPrivateKey(exchange, opts.privateKey);
207
+ const walletName = opts.wallet;
208
+ const pk = await tryLoadPrivateKey(exchange, opts.privateKey, walletName);
212
209
  switch (exchange) {
213
210
  case "pacifica": {
214
211
  if (_pacificaAdapter)
@@ -287,7 +284,8 @@ async function getHLAdapterForDex(dex) {
287
284
  if (_dexAdapters.has(dex))
288
285
  return _dexAdapters.get(dex);
289
286
  const opts = program.opts();
290
- const pk = await tryLoadPrivateKey("hyperliquid", opts.privateKey);
287
+ const walletName = opts.wallet;
288
+ const pk = await tryLoadPrivateKey("hyperliquid", opts.privateKey, walletName);
291
289
  const adapter = new HyperliquidAdapter(pk ?? undefined, opts.network === "testnet");
292
290
  if (dex !== "hl")
293
291
  adapter.setDex(dex);
@@ -297,125 +295,43 @@ async function getHLAdapterForDex(dex) {
297
295
  }
298
296
  registerArbAutoCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex);
299
297
  registerArbManageCommands(program, getAdapterForExchange, isJson);
300
- registerGapCommands(program, isJson);
301
298
  registerAgentCommands(program, getAdapter, isJson);
302
- registerWithdrawCommands(program, getAdapter, isJson);
299
+ // withdraw merged into funds
303
300
  registerRebalanceCommands(program, getAdapterForExchange, isJson);
304
301
  // Jobs & strategies
305
302
  import { registerJobsCommands } from "./commands/jobs.js";
306
- import { registerRunCommands } from "./commands/run.js";
307
303
  registerJobsCommands(program, isJson);
308
- registerRunCommands(program, getAdapter, getAdapterForExchange, isJson);
309
304
  registerBotCommands(program, getAdapter, getAdapterForExchange, isJson);
310
305
  // Agent-friendly commands
311
- registerHealthCommands(program, isJson);
312
- registerPortfolioCommands(program, getAdapterForExchange, isJson);
313
306
  registerRiskCommands(program, getAdapterForExchange, isJson);
314
- registerHistoryCommands(program, isJson);
315
- registerAnalyticsCommands(program, getAdapterForExchange, isJson);
316
- registerPnlCommands(program, getAdapterForExchange, isJson);
317
- registerMultilegCommands(program, getAdapterForExchange, isJson);
307
+ registerHistoryCommands(program, isJson, getAdapterForExchange);
318
308
  registerSettingsCommands(program, isJson, getAdapterForExchange);
319
- registerDexCommands(program, getAdapter, isJson);
309
+ // dex commands removed — use 'market hip3' + --dex flag
320
310
  registerPlanCommands(program, getAdapter, isJson);
321
- registerFundingCommands(program, isJson, getAdapterForExchange);
311
+ // funding merged into arb — registerFundingCommands removed
322
312
  registerBacktestCommands(program, isJson);
323
313
  registerDashboardCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex);
324
314
  registerInitCommand(program);
325
- registerEnvCommands(program, isJson);
326
- // Agent discovery: perp api-spec — returns full CLI spec as JSON
327
- program
315
+ // Agent discovery: perp api-spec — deprecated, use 'perp agent schema'
316
+ const apiSpecCmd = program
328
317
  .command("api-spec")
329
- .description("Return full CLI command spec as JSON (for agent discovery)")
318
+ .description("Use 'perp agent schema'")
330
319
  .action(async () => {
331
320
  const { jsonOk, printJson } = await import("./utils.js");
332
321
  const { getCliSpec } = await import("./cli-spec.js");
333
322
  printJson(jsonOk(getCliSpec(program)));
334
323
  });
335
- // Status command
336
- program
337
- .command("status")
338
- .description("Quick overview: account + positions + open orders")
339
- .action(async () => {
340
- const adapter = await getAdapter();
341
- const json = isJson();
342
- try {
343
- // Use allSettled to handle unfunded/404 accounts gracefully
344
- const [balanceResult, positionsResult, ordersResult] = await Promise.allSettled([
345
- adapter.getBalance(),
346
- adapter.getPositions(),
347
- adapter.getOpenOrders(),
348
- ]);
349
- const balance = balanceResult.status === "fulfilled" ? balanceResult.value : { equity: "0", available: "0", marginUsed: "0", unrealizedPnl: "0" };
350
- const positions = positionsResult.status === "fulfilled" ? positionsResult.value : [];
351
- const orders = ordersResult.status === "fulfilled" ? ordersResult.value : [];
352
- // Collect errors for reporting
353
- const errors = [];
354
- if (balanceResult.status === "rejected")
355
- errors.push(`balance: ${balanceResult.reason instanceof Error ? balanceResult.reason.message : String(balanceResult.reason)}`);
356
- if (positionsResult.status === "rejected")
357
- errors.push(`positions: ${positionsResult.reason instanceof Error ? positionsResult.reason.message : String(positionsResult.reason)}`);
358
- if (ordersResult.status === "rejected")
359
- errors.push(`orders: ${ordersResult.reason instanceof Error ? ordersResult.reason.message : String(ordersResult.reason)}`);
360
- // Detect unfunded account (all calls failed, likely 404)
361
- const allFailed = errors.length === 3;
362
- const isUnfunded = allFailed && errors.some(e => e.includes("404") || e.includes("not found") || e.includes("does not exist"));
363
- if (json) {
364
- const { jsonOk, printJson } = await import("./utils.js");
365
- return printJson(jsonOk({
366
- exchange: adapter.name,
367
- balance,
368
- positions,
369
- orders,
370
- ...(isUnfunded ? { warning: "Account not yet initialized on this exchange. Deposit funds to get started." } : {}),
371
- ...(errors.length > 0 && !isUnfunded ? { errors } : {}),
372
- }));
373
- }
374
- console.log(chalk.cyan.bold(`\n ${adapter.name.toUpperCase()} Account Status\n`));
375
- if (isUnfunded) {
376
- console.log(chalk.yellow(" Account not yet initialized on this exchange."));
377
- console.log(chalk.gray(" Deposit funds to get started: perp deposit <exchange> <amount>\n"));
378
- }
379
- else {
380
- console.log(` Equity: $${Number(balance.equity).toFixed(2)}`);
381
- console.log(` Available: $${Number(balance.available).toFixed(2)}`);
382
- console.log(` Margin Used: $${Number(balance.marginUsed).toFixed(2)}`);
383
- console.log(` Positions: ${positions.length}`);
384
- console.log(` Open Orders: ${orders.length}`);
385
- if (positions.length > 0) {
386
- console.log(chalk.cyan.bold("\n Positions:"));
387
- positions.forEach((p) => {
388
- const color = p.side === "long" ? chalk.green : chalk.red;
389
- const pnlNum = Number(p.unrealizedPnl);
390
- const pnlColor = pnlNum >= 0 ? chalk.green : chalk.red;
391
- 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)}`);
392
- });
393
- }
394
- if (orders.length > 0) {
395
- console.log(chalk.cyan.bold("\n Open Orders:"));
396
- orders.forEach((o) => {
397
- const color = o.side === "buy" ? chalk.green : chalk.red;
398
- 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}`);
399
- });
400
- }
401
- if (errors.length > 0) {
402
- console.log(chalk.yellow(`\n Warnings: ${errors.join(", ")}`));
403
- }
404
- }
405
- console.log();
406
- }
407
- catch (err) {
408
- const msg = err instanceof Error ? err.message : String(err);
409
- if (isJson()) {
410
- const { jsonError } = await import("./utils.js");
411
- console.log(JSON.stringify(jsonError("COMMAND_ERROR", msg)));
412
- }
413
- else {
414
- console.error(chalk.red(`Error: ${msg}`));
415
- }
416
- 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);
417
331
  }
332
+ console.log("Use 'perp portfolio' or 'perp account' instead.");
418
333
  });
334
+ statusCmd._hidden = true;
419
335
  // Switch shared API URLs if --network testnet is used
420
336
  program.hook("preAction", () => {
421
337
  const network = program.opts().network;