perp-cli 0.4.12 → 0.5.2

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.
@@ -72,17 +72,38 @@ export class HyperliquidAdapter {
72
72
  async _loadAssetMap() {
73
73
  try {
74
74
  if (this._dex) {
75
- // HIP-3 deployed dex: use raw info POST with dex param
76
- const meta = await this._infoPost({ type: "meta", dex: this._dex });
75
+ // HIP-3 deployed dex: asset indices use global offset scheme
76
+ // offset = 110000 + dexListIndex * 10000 (per Python SDK convention)
77
+ const [meta, dexList] = await Promise.all([
78
+ this._infoPost({ type: "meta", dex: this._dex }),
79
+ this._infoPost({ type: "perpDexs" }),
80
+ ]);
81
+ // Find this dex's position in the list (index 0 is native/null)
82
+ let dexOffset = 0;
83
+ for (let i = 1; i < dexList.length; i++) {
84
+ if (dexList[i]?.name === this._dex) {
85
+ dexOffset = 110000 + (i - 1) * 10000;
86
+ break;
87
+ }
88
+ }
89
+ if (dexOffset === 0) {
90
+ console.error(`[hyperliquid] Dex "${this._dex}" not found in perpDexs list`);
91
+ }
77
92
  if (meta?.universe) {
78
93
  meta.universe.forEach((asset, idx) => {
94
+ const globalIdx = dexOffset + idx;
79
95
  // Store both with and without dex prefix for flexible lookup
80
96
  // API returns "km:GOOGL" but callers may use "GOOGL" or "KM:GOOGL"
81
97
  const fullName = asset.name; // e.g., "km:GOOGL"
82
98
  const baseName = fullName.includes(":") ? fullName.split(":").pop() : fullName;
83
- this._assetMap.set(fullName, idx);
84
- this._assetMap.set(baseName, idx);
85
- this._assetMapReverse.set(idx, fullName);
99
+ this._assetMap.set(fullName, globalIdx);
100
+ this._assetMap.set(baseName, globalIdx);
101
+ this._assetMapReverse.set(globalIdx, fullName);
102
+ // Cache szDecimals for dex assets (critical for order sizing)
103
+ if (asset.szDecimals !== undefined) {
104
+ this._szDecimalsMap.set(fullName, asset.szDecimals);
105
+ this._szDecimalsMap.set(baseName, asset.szDecimals);
106
+ }
86
107
  });
87
108
  }
88
109
  }
@@ -168,8 +189,8 @@ export class HyperliquidAdapter {
168
189
  ctxs = meta[1] ?? [];
169
190
  mids = allMids;
170
191
  }
171
- // Rebuild asset map if empty
172
- if (this._assetMap.size === 0) {
192
+ // Rebuild asset map if empty (fallback — normally populated by init/_loadAssetMap)
193
+ if (this._assetMap.size === 0 && !this._dex) {
173
194
  universe.forEach((asset, i) => {
174
195
  const sym = String(asset.name);
175
196
  this._assetMap.set(sym, i);
@@ -191,8 +212,24 @@ export class HyperliquidAdapter {
191
212
  });
192
213
  }
193
214
  async getOrderbook(symbol) {
194
- const book = await this.sdk.info.getL2Book(symbol.toUpperCase());
195
- const levels = book?.levels ?? [[], []];
215
+ let levels;
216
+ if (this._dex) {
217
+ // HIP-3 dex: SDK's getL2Book() doesn't support dex parameter
218
+ // Resolve symbol to full dex-prefixed name (e.g. "US500" → "km:US500")
219
+ const resolved = this.resolveSymbol(symbol.toUpperCase());
220
+ // Ensure dex prefix is present (API requires it)
221
+ const coin = resolved.includes(":") ? resolved : `${this._dex}:${resolved}`;
222
+ const book = await this._infoPost({
223
+ type: "l2Book",
224
+ coin,
225
+ dex: this._dex,
226
+ });
227
+ levels = book?.levels ?? [[], []];
228
+ }
229
+ else {
230
+ const book = await this.sdk.info.getL2Book(symbol.toUpperCase());
231
+ levels = book?.levels ?? [[], []];
232
+ }
196
233
  return {
197
234
  bids: (levels[0] ?? []).map((l) => [
198
235
  String(l.px ?? "0"),
@@ -291,6 +328,22 @@ export class HyperliquidAdapter {
291
328
  }
292
329
  async getOpenOrders() {
293
330
  this.ensureAddress();
331
+ if (this._dex) {
332
+ // HIP-3 dex: use frontendOpenOrders with dex parameter
333
+ const orders = await this._infoPost({
334
+ type: "frontendOpenOrders", user: this._address, dex: this._dex,
335
+ });
336
+ return (orders ?? []).map((o) => ({
337
+ orderId: String(o.oid ?? ""),
338
+ symbol: String(o.coin ?? ""),
339
+ side: String(o.side) === "B" ? "buy" : String(o.side) === "A" ? "sell" : String(o.side).toLowerCase(),
340
+ price: String(o.limitPx ?? "0"),
341
+ size: String(o.sz ?? "0"),
342
+ filled: "0",
343
+ status: "open",
344
+ type: "limit",
345
+ }));
346
+ }
294
347
  const orders = await this.sdk.info.getUserOpenOrders(this._address);
295
348
  return (orders ?? []).map((o) => ({
296
349
  orderId: String(o.oid ?? ""),
@@ -337,16 +390,20 @@ export class HyperliquidAdapter {
337
390
  */
338
391
  async _dexMarketOrder(symbol, side, size) {
339
392
  const assetIndex = await this.getAssetIndex(symbol.toUpperCase());
340
- // Get current mark price + szDecimals from dex meta
393
+ const szDec = this.getSzDecimals(symbol);
394
+ // Get current mark price from dex meta (lookup by coin name, not array index)
395
+ const resolved = this.resolveSymbol(symbol.toUpperCase());
396
+ const coin = resolved.includes(":") ? resolved : `${this._dex}:${resolved}`;
341
397
  const meta = await this._infoPost({
342
398
  type: "metaAndAssetCtxs",
343
399
  dex: this._dex,
344
400
  });
345
- const ctx = (meta[1] ?? [])[assetIndex];
401
+ // Find local index by matching coin name in universe
402
+ const localIdx = meta[0]?.universe?.findIndex(a => a.name === coin) ?? -1;
403
+ const ctx = localIdx >= 0 ? (meta[1] ?? [])[localIdx] : undefined;
346
404
  const midPrice = Number(ctx?.markPx ?? 0);
347
405
  if (midPrice <= 0)
348
406
  throw new Error(`Cannot get price for ${symbol} on dex ${this._dex}`);
349
- const szDec = meta[0]?.universe?.[assetIndex]?.szDecimals ?? 2;
350
407
  // HL official rounding: 5 significant figures, max 6 decimals for perps
351
408
  const MAX_DECIMALS_PERP = 6;
352
409
  const slippage = 0.05;
@@ -373,6 +430,40 @@ export class HyperliquidAdapter {
373
430
  throw new Error("No private key configured. Run: perp init");
374
431
  }
375
432
  }
433
+ /**
434
+ * Normalize trailing zeros from price ('p') and size ('s') string fields
435
+ * in an action object, recursively. Matches the SDK's normalizeTrailingZeros.
436
+ *
437
+ * The HL validator normalizes these fields before hashing; if we hash without
438
+ * normalizing, the recovered signer address won't match → "User or API Wallet
439
+ * does not exist" error.
440
+ */
441
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
442
+ static _normalizeAction(obj) {
443
+ if (!obj || typeof obj !== "object")
444
+ return obj;
445
+ if (Array.isArray(obj)) {
446
+ return obj.map(item => HyperliquidAdapter._normalizeAction(item));
447
+ }
448
+ const result = { ...obj };
449
+ for (const key in result) {
450
+ if (!Object.prototype.hasOwnProperty.call(result, key))
451
+ continue;
452
+ const value = result[key];
453
+ if (value && typeof value === "object") {
454
+ result[key] = HyperliquidAdapter._normalizeAction(value);
455
+ }
456
+ else if ((key === "p" || key === "s") && typeof value === "string") {
457
+ let v = value;
458
+ if (v.includes("."))
459
+ v = v.replace(/\.?0+$/, "");
460
+ if (v === "-0")
461
+ v = "0";
462
+ result[key] = v;
463
+ }
464
+ }
465
+ return result;
466
+ }
376
467
  async _signAndSendAction(action) {
377
468
  this.ensureSigner();
378
469
  const { encode } = await import("@msgpack/msgpack");
@@ -381,9 +472,13 @@ export class HyperliquidAdapter {
381
472
  const baseUrl = isMainnet
382
473
  ? "https://api.hyperliquid.xyz"
383
474
  : "https://api.hyperliquid-testnet.xyz";
475
+ // Normalize trailing zeros from 'p' and 's' fields before hashing.
476
+ // The HL validator does the same; without this, the hash differs and the
477
+ // recovered signer address is wrong → "User or API Wallet does not exist".
478
+ const normalizedAction = HyperliquidAdapter._normalizeAction(action);
384
479
  // Sign L1 action (replicates SDK's signL1Action)
385
480
  const nonce = Date.now();
386
- const msgPackBytes = encode(action);
481
+ const msgPackBytes = encode(normalizedAction);
387
482
  const data = new Uint8Array(msgPackBytes.length + 9); // +8 nonce +1 vault flag
388
483
  data.set(msgPackBytes);
389
484
  const view = new DataView(data.buffer);
@@ -419,7 +514,14 @@ export class HyperliquidAdapter {
419
514
  headers: { "Content-Type": "application/json" },
420
515
  body: JSON.stringify(payload),
421
516
  });
422
- const result = await res.json();
517
+ const text = await res.text();
518
+ let result;
519
+ try {
520
+ result = JSON.parse(text);
521
+ }
522
+ catch {
523
+ throw new Error(`HL exchange API error (${res.status}): ${text.slice(0, 200)}`);
524
+ }
423
525
  if (result?.status === "err") {
424
526
  throw new Error(result.response ?? JSON.stringify(result));
425
527
  }
@@ -450,10 +552,7 @@ export class HyperliquidAdapter {
450
552
  orders: [orderWire],
451
553
  grouping: "na",
452
554
  };
453
- // Add dex field for HIP-3 deployed perps
454
- if (this._dex) {
455
- action.dex = this._dex;
456
- }
555
+ // No "dex" field needed — the global asset index (110000+) encodes the dex
457
556
  return this._signAndSendAction(action);
458
557
  }
459
558
  async limitOrder(symbol, side, price, size, opts) {
@@ -480,6 +579,16 @@ export class HyperliquidAdapter {
480
579
  }
481
580
  async cancelOrder(symbol, orderId) {
482
581
  this.ensureSigner();
582
+ if (this._dex) {
583
+ // HIP-3 dex: use raw cancel action with global asset index
584
+ const assetIndex = await this.getAssetIndex(symbol.toUpperCase());
585
+ const result = await this._signAndSendAction({
586
+ type: "cancel",
587
+ cancels: [{ a: assetIndex, o: parseInt(orderId) }],
588
+ });
589
+ await this._invalidateAccountCache();
590
+ return result;
591
+ }
483
592
  // HL SDK expects coin in "ETH-PERP" format (internal symbol convention)
484
593
  const resolved = this.resolveSymbol(symbol);
485
594
  const result = await this.sdk.exchange.cancelOrder({
@@ -494,16 +603,25 @@ export class HyperliquidAdapter {
494
603
  const orders = await this.getOpenOrders();
495
604
  const toCancel = symbol
496
605
  ? orders.filter((o) => {
497
- const norm = (s) => s.toUpperCase().replace(/-PERP$/, "");
606
+ const norm = (s) => s.toUpperCase().replace(/-PERP$/, "").replace(/^[^:]+:/, "");
498
607
  return norm(o.symbol) === norm(symbol);
499
608
  })
500
609
  : orders;
501
610
  const results = [];
502
611
  for (const o of toCancel) {
503
- results.push(await this.sdk.exchange.cancelOrder({
504
- coin: o.symbol,
505
- o: parseInt(o.orderId),
506
- }));
612
+ if (this._dex) {
613
+ const assetIndex = await this.getAssetIndex(o.symbol);
614
+ results.push(await this._signAndSendAction({
615
+ type: "cancel",
616
+ cancels: [{ a: assetIndex, o: parseInt(o.orderId) }],
617
+ }));
618
+ }
619
+ else {
620
+ results.push(await this.sdk.exchange.cancelOrder({
621
+ coin: o.symbol,
622
+ o: parseInt(o.orderId),
623
+ }));
624
+ }
507
625
  }
508
626
  return results;
509
627
  }
@@ -608,22 +726,37 @@ export class HyperliquidAdapter {
608
726
  */
609
727
  async triggerOrder(symbol, side, size, triggerPrice, tpsl, opts) {
610
728
  this.ensureSigner();
611
- const orderParams = {
612
- coin: symbol.toUpperCase(),
613
- is_buy: side === "buy",
614
- sz: parseFloat(size),
615
- limit_px: parseFloat(triggerPrice),
616
- order_type: {
729
+ const assetIndex = await this.getAssetIndex(symbol.toUpperCase());
730
+ const szDec = this.getSzDecimals(symbol);
731
+ const roundedSize = Number(size).toFixed(szDec);
732
+ const roundedTrigger = this._hlRoundPrice(Number(triggerPrice), szDec);
733
+ // Remove trailing zeros (HL API requirement)
734
+ const trimZeros = (s) => {
735
+ if (!s.includes("."))
736
+ return s;
737
+ const n = s.replace(/\.?0+$/, "");
738
+ return n === "-0" ? "0" : n;
739
+ };
740
+ const orderWire = {
741
+ a: assetIndex,
742
+ b: side === "buy",
743
+ p: trimZeros(roundedTrigger),
744
+ s: trimZeros(roundedSize),
745
+ r: opts?.reduceOnly ?? true,
746
+ t: {
617
747
  trigger: {
618
- triggerPx: triggerPrice,
619
748
  isMarket: opts?.isMarket ?? true,
749
+ triggerPx: trimZeros(roundedTrigger),
620
750
  tpsl,
621
751
  },
622
752
  },
623
- reduce_only: opts?.reduceOnly ?? true,
753
+ };
754
+ const action = {
755
+ type: "order",
756
+ orders: [orderWire],
624
757
  grouping: opts?.grouping ?? "positionTpsl",
625
758
  };
626
- return this.sdk.exchange.placeOrder(orderParams);
759
+ return this._signAndSendAction(action);
627
760
  }
628
761
  /**
629
762
  * Place a TWAP order.
@@ -240,10 +240,19 @@ export class LighterAdapter {
240
240
  if (!dec) {
241
241
  throw new Error(`No market decimals loaded for ${symbol}. Market data may not be initialized.`);
242
242
  }
243
- return {
244
- baseAmount: Math.round(size * Math.pow(10, dec.size)),
245
- priceTicks: Math.round(price * Math.pow(10, dec.price)),
246
- };
243
+ const baseAmount = Math.round(size * Math.pow(10, dec.size));
244
+ const priceTicks = Math.round(price * Math.pow(10, dec.price));
245
+ // Validate: size too small for this market's precision → baseAmount rounds to 0
246
+ if (size > 0 && baseAmount <= 0) {
247
+ const minSize = 1 / Math.pow(10, dec.size);
248
+ throw new Error(`Order size ${size} too small for ${symbol} (sizeDecimals=${dec.size}, min=${minSize}). ` +
249
+ `Increase size or use a different market.`);
250
+ }
251
+ if (price > 0 && priceTicks <= 0) {
252
+ const minPrice = 1 / Math.pow(10, dec.price);
253
+ throw new Error(`Order price ${price} too small for ${symbol} (priceDecimals=${dec.price}, min=${minPrice}).`);
254
+ }
255
+ return { baseAmount, priceTicks };
247
256
  }
248
257
  async getMarkets() {
249
258
  const markets = [];
@@ -338,9 +347,14 @@ export class LighterAdapter {
338
347
  : "0"),
339
348
  liquidationPrice: String(p.liquidation_price || "N/A"),
340
349
  unrealizedPnl: String(p.unrealized_pnl || "0"),
341
- leverage: Number(p.initial_margin_fraction || 0) > 0
342
- ? Math.round(10000 / Number(p.initial_margin_fraction))
343
- : 1,
350
+ // Compute actual leverage = notional / account equity (not max leverage from IMF)
351
+ leverage: (() => {
352
+ const notional = Math.abs(Number(p.position_value || 0));
353
+ const equity = Number(acct.total_asset_value || 0);
354
+ if (equity > 0 && notional > 0)
355
+ return Math.round(notional / equity * 10) / 10;
356
+ return 1;
357
+ })(),
344
358
  };
345
359
  });
346
360
  }
@@ -584,7 +598,7 @@ export class LighterAdapter {
584
598
  // /recentTrades returns is_maker_ask: maker was asking → taker bought
585
599
  side: t.is_maker_ask ? "buy" : "sell",
586
600
  price: String(t.price ?? "0"),
587
- size: String(t.base_amount ?? t.amount ?? ""),
601
+ size: String(t.size ?? t.base_amount ?? t.amount ?? ""),
588
602
  fee: String(t.fee ?? "0"),
589
603
  }));
590
604
  }
package/dist/liquidity.js CHANGED
@@ -41,13 +41,15 @@ export function computeExecutableSize(levels, requestedSizeUsd, maxSlippagePct =
41
41
  const avgFillPrice = filledSize > 0 ? filledNotional / filledSize : bestPrice;
42
42
  const slippagePct = bestPrice > 0 ? ((avgFillPrice - bestPrice) / bestPrice) * 100 : 0;
43
43
  const canFillFull = filledNotional >= requestedSizeUsd * 0.95; // 95% fill = close enough
44
+ // 10% safety buffer — avoid filling at the very edge of liquidity
45
+ const bufferedSize = filledSize * 0.9;
44
46
  return {
45
47
  maxSize: filledSize,
46
48
  avgFillPrice,
47
49
  slippagePct: Math.abs(slippagePct),
48
50
  depthUsd: totalDepthUsd,
49
51
  canFillFull,
50
- recommendedSize: filledSize,
52
+ recommendedSize: bufferedSize,
51
53
  };
52
54
  }
53
55
  /**
@@ -339,15 +339,15 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
339
339
  steps.push({ step: 1, command: `perp -e ${ex} --json account orders`, description: "List open orders to find order ID" }, { step: 2, command: `perp -e ${ex} --json trade edit <symbol> <orderId> <newPrice> <newSize>`, description: "Modify the order", dangerous: true });
340
340
  }
341
341
  else if (g.includes("arb") || g.includes("arbitrage")) {
342
- steps.push({ step: 1, command: "perp --json arb scan --min 5", description: "Scan funding rate arbitrage opportunities" }, { step: 2, command: "perp --json arb scan", description: "Find high-spread opportunities" }, { step: 3, command: "perp --json gap show", description: "Check cross-exchange price gaps" }, { step: 4, command: "perp --json arb dex", description: "HIP-3 cross-dex arb opportunities (Hyperliquid)" });
342
+ steps.push({ step: 1, command: "perp --json arb scan --min 5", description: "Scan funding rate arbitrage opportunities" }, { step: 2, command: "perp --json arb scan --gaps", description: "Check cross-exchange price gaps" }, { step: 3, command: "perp --json arb scan --hip3", description: "HIP-3 cross-dex arb opportunities (Hyperliquid)" });
343
343
  }
344
344
  else if (g.includes("funding")) {
345
345
  const symbol = extractSymbol(g);
346
346
  if (symbol) {
347
- steps.push({ step: 1, command: `perp --json arb rates`, description: `Funding rates across all exchanges` }, { step: 2, command: `perp -e ${ex} --json market funding ${symbol}`, description: `${symbol} funding history` });
347
+ steps.push({ step: 1, command: `perp --json arb scan --rates`, description: `Funding rates across all exchanges` }, { step: 2, command: `perp -e ${ex} --json market funding ${symbol}`, description: `${symbol} funding history` });
348
348
  }
349
349
  else {
350
- steps.push({ step: 1, command: "perp --json arb rates", description: "Funding rates across all exchanges" }, { step: 2, command: "perp --json arb scan --min 5", description: "Funding rate arb opportunities" });
350
+ steps.push({ step: 1, command: "perp --json arb scan --rates", description: "Funding rates across all exchanges" }, { step: 2, command: "perp --json arb scan --min 5", description: "Funding rate arb opportunities" });
351
351
  }
352
352
  }
353
353
  else if (g.includes("bridge") || g.includes("transfer") || g.includes("cross-chain")) {
@@ -386,22 +386,22 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
386
386
  steps.push({ step: 1, command: `perp -e ${ex} --json risk status`, description: "Portfolio risk overview" }, { step: 2, command: `perp -e ${ex} --json risk limits`, description: "Current position limits" });
387
387
  }
388
388
  else if (g.includes("analytic") || g.includes("performance") || g.includes("pnl") || g.includes("p&l")) {
389
- steps.push({ step: 1, command: `perp -e ${ex} --json analytics summary`, description: "Trading performance summary" }, { step: 2, command: `perp -e ${ex} --json analytics pnl`, description: "P&L breakdown" }, { step: 3, command: `perp -e ${ex} --json analytics funding`, description: "Funding payment history" });
389
+ steps.push({ step: 1, command: `perp -e ${ex} --json history summary`, description: "Trading performance summary" }, { step: 2, command: `perp -e ${ex} --json history pnl`, description: "P&L breakdown" }, { step: 3, command: `perp -e ${ex} --json history funding`, description: "Funding payment history" });
390
390
  }
391
391
  else if (g.includes("history") || g.includes("log") || g.includes("audit")) {
392
392
  steps.push({ step: 1, command: `perp -e ${ex} --json history list`, description: "Execution audit trail" }, { step: 2, command: `perp -e ${ex} --json history stats`, description: "Execution statistics" });
393
393
  }
394
- else if (g.includes("stream") || g.includes("live") || g.includes("watch") || g.includes("realtime")) {
394
+ else if (g.includes("live") || g.includes("watch") || g.includes("realtime")) {
395
395
  const symbol = extractSymbol(g);
396
396
  if (symbol) {
397
- steps.push({ step: 1, command: `perp -e ${ex} stream book ${symbol}`, description: `Live ${symbol} orderbook` }, { step: 2, command: `perp -e ${ex} stream trades ${symbol}`, description: `Live ${symbol} trades` });
397
+ steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `${symbol} orderbook` }, { step: 2, command: `perp -e ${ex} --json market trades ${symbol}`, description: `${symbol} recent trades` }, { step: 3, command: `perp --json arb scan --gaps --live`, description: "Live cross-exchange price gap monitor" });
398
398
  }
399
399
  else {
400
- steps.push({ step: 1, command: `perp -e ${ex} stream prices`, description: "Live price stream" }, { step: 2, command: `perp -e ${ex} stream events`, description: "Live account events (fills, liquidations)" });
400
+ steps.push({ step: 1, command: "perp --json arb scan --gaps --live", description: "Live cross-exchange price gap monitor" }, { step: 2, command: `perp -e ${ex} --json market prices`, description: "Current prices across exchanges" });
401
401
  }
402
402
  }
403
403
  else if (g.includes("gap")) {
404
- steps.push({ step: 1, command: "perp --json gap show", description: "Cross-exchange price gaps" }, { step: 2, command: "perp --json gap watch", description: "Live gap monitor" });
404
+ steps.push({ step: 1, command: "perp --json arb scan --gaps", description: "Cross-exchange price gaps" }, { step: 2, command: "perp --json arb scan --gaps --live", description: "Live gap monitor" });
405
405
  }
406
406
  else if (g.includes("rebalance")) {
407
407
  steps.push({ step: 1, command: "perp --json rebalance check", description: "Check balance distribution" }, { step: 2, command: "perp --json rebalance plan", description: "Generate rebalance plan" }, { step: 3, command: "perp --json rebalance execute", description: "Execute rebalance", dangerous: true });
@@ -446,7 +446,7 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
446
446
  steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `${symbol} orderbook` }, { step: 2, command: `perp -e ${ex} --json market funding ${symbol}`, description: `${symbol} funding history` }, { step: 3, command: `perp -e ${ex} --json market kline ${symbol} 1h`, description: `${symbol} hourly candles` });
447
447
  }
448
448
  else {
449
- steps.push({ step: 1, command: "perp --json market prices", description: "All market prices" }, { step: 2, command: "perp --json gap show", description: "Cross-exchange price gaps" });
449
+ steps.push({ step: 1, command: "perp --json market prices", description: "All market prices" }, { step: 2, command: "perp --json arb scan --gaps", description: "Cross-exchange price gaps" });
450
450
  }
451
451
  }
452
452
  else if (g.includes("setting") || g.includes("config") || g.includes("referral")) {
@@ -708,6 +708,8 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
708
708
  history: "Shows order history (filled, cancelled, etc.)",
709
709
  trades: "Shows trade execution history with prices and fees",
710
710
  "funding-history": "Shows funding payments received/paid",
711
+ funding: "Shows funding payments received/paid (alias for funding-history)",
712
+ "twap-orders": "Shows active TWAP orders",
711
713
  pnl: "Shows profit & loss summary",
712
714
  margin: `Shows margin info for ${args[2] || "a symbol"}`,
713
715
  settings: "Shows account settings (leverage, margin mode per symbol)",
@@ -715,26 +717,26 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
715
717
  parameters: [],
716
718
  risks: [],
717
719
  category: "read",
718
- relatedCommands: ["perp portfolio", "perp portfolio"],
720
+ relatedCommands: ["perp portfolio", "perp account balance"],
719
721
  };
720
722
  }
721
723
  else if (category === "arb") {
722
724
  explanation = {
723
725
  command,
724
726
  description: {
725
- rates: "Compares funding rates across all 3 exchanges",
726
- scan: "Scans for funding rate arbitrage opportunities with the largest spreads",
727
- funding: "Detailed funding rate analysis across exchanges",
728
- dex: "HIP-3 cross-dex arbitrage opportunities on Hyperliquid",
727
+ scan: "Scans for funding rate arbitrage opportunities. Use --rates for funding rates, --gaps for price gaps, --hip3 for cross-dex arb, --positions for funding impact, --basis for basis trading",
729
728
  auto: "Auto-execute funding rate arbitrage (runs as background job)",
729
+ exec: "Execute an arb trade (enter paired positions on two exchanges)",
730
730
  status: "Shows current arb positions and P&L",
731
731
  close: "Closes an arb position on both exchanges",
732
732
  history: "Shows arb execution history",
733
+ config: "View/edit arb configuration",
734
+ rebalance: "Rebalance funds across exchanges for arb positions",
733
735
  }[sub] || `Arbitrage command: ${sub}`,
734
736
  parameters: [],
735
- risks: ["auto" === sub ? "Auto-executes trades on both exchanges" : ""].filter(Boolean),
736
- category: sub === "auto" || sub === "close" ? "write" : "analysis",
737
- relatedCommands: ["perp arb scan", "perp arb scan", "perp gap show"],
737
+ risks: [sub === "auto" || sub === "exec" ? "Executes trades on exchanges" : ""].filter(Boolean),
738
+ category: sub === "auto" || sub === "close" || sub === "exec" || sub === "rebalance" ? "write" : "analysis",
739
+ relatedCommands: ["perp arb scan", "perp arb status", "perp arb scan --gaps"],
738
740
  };
739
741
  }
740
742
  else if (category === "bridge") {
@@ -774,16 +776,6 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
774
776
  relatedCommands: ["perp account positions", "perp portfolio"],
775
777
  };
776
778
  }
777
- else if (category === "alert") {
778
- explanation = {
779
- command,
780
- description: { add: "Add a price, funding rate, PnL, or liquidation alert", list: "List all active alerts", remove: "Remove an alert by ID", daemon: "Start the alert monitoring daemon", test: "Send a test alert notification", config: "Configure alert notification channels (Telegram, Discord)" }[sub] || `Alert command: ${sub}`,
781
- parameters: [],
782
- risks: [],
783
- category: "read",
784
- relatedCommands: ["perp risk status", "perp risk limits"],
785
- };
786
- }
787
779
  else if (category === "bot" || category === "run") {
788
780
  explanation = {
789
781
  command,
@@ -797,9 +789,9 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
797
789
  else if (category === "history") {
798
790
  explanation = {
799
791
  command,
800
- description: { summary: "Trading performance summary", pnl: "P&L breakdown by symbol and time period", funding: "Funding payment history and totals", report: "Detailed performance report", perf: "Performance breakdown (daily/weekly/summary)" }[sub] || `History: ${sub}`,
792
+ description: { list: "Execution audit trail", stats: "Execution statistics", positions: "Position history", summary: "Trading performance summary", pnl: "P&L breakdown by symbol and time period", funding: "Funding payment history and totals", report: "Detailed performance report", snapshot: "Portfolio snapshot", track: "Track position over time", perf: "Performance breakdown (daily/weekly/summary)", prune: "Prune old execution records" }[sub] || `History: ${sub}`,
801
793
  parameters: [],
802
- risks: [],
794
+ risks: sub === "prune" ? ["Permanently removes old execution records"] : [],
803
795
  category: "read",
804
796
  relatedCommands: ["perp history summary", "perp history list"],
805
797
  };
@@ -807,21 +799,11 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
807
799
  else if (category === "wallet") {
808
800
  explanation = {
809
801
  command,
810
- description: { list: "List configured wallets", balance: "Check on-chain USDC/SOL/ETH balances", generate: "Generate a new wallet keypair", use: "Set active wallet" }[sub] || `Wallet: ${sub}`,
802
+ description: { show: "Show configured wallets with public addresses", list: "List configured wallets", balance: "Check on-chain USDC/SOL/ETH balances", generate: "Generate a new wallet keypair", import: "Import an existing private key", set: "Set private key for an exchange", use: "Set active wallet", remove: "Remove a wallet", rename: "Rename a wallet" }[sub] || `Wallet: ${sub}`,
811
803
  parameters: [],
812
- risks: sub === "generate" ? ["Store the private key securely — it cannot be recovered"] : [],
804
+ risks: sub === "generate" ? ["Store the private key securely — it cannot be recovered"] : sub === "remove" ? ["Wallet will be removed — ensure you have the private key backed up"] : [],
813
805
  category: "read",
814
- relatedCommands: ["perp wallet list", "perp wallet balance"],
815
- };
816
- }
817
- else if (category === "gap") {
818
- explanation = {
819
- command,
820
- description: { show: "Shows cross-exchange price gaps for all symbols", watch: "Live monitor for price gaps above threshold", track: "Track gap history", alert: "Alert when gaps exceed threshold" }[sub] || `Gap analysis: ${sub}`,
821
- parameters: [],
822
- risks: [],
823
- category: "analysis",
824
- relatedCommands: ["perp gap show", "perp arb scan"],
806
+ relatedCommands: ["perp wallet show", "perp wallet balance"],
825
807
  };
826
808
  }
827
809
  else if (category === "backtest") {
@@ -976,7 +958,8 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
976
958
  orders: { usage: "perp account orders", description: "Open/pending orders" },
977
959
  history: { usage: "perp account history", description: "Order history" },
978
960
  trades: { usage: "perp account trades", description: "Trade fill history" },
979
- "funding-history": { usage: "perp account funding-history", description: "Funding payments" },
961
+ "funding-history|funding": { usage: "perp account funding-history", description: "Funding payments" },
962
+ "twap-orders": { usage: "perp account twap-orders", description: "Active TWAP orders" },
980
963
  pnl: { usage: "perp account pnl", description: "Profit & loss" },
981
964
  margin: { usage: "perp account margin <symbol>", description: "Position margin info" },
982
965
  settings: { usage: "perp account settings", description: "Account settings (leverage per symbol)" },
@@ -1007,19 +990,23 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
1007
990
  fills: { usage: "perp trade fills [symbol]", description: "Recent fills" },
1008
991
  status: { usage: "perp trade status <orderId>", description: "Check order status" },
1009
992
  "pnl-track": { usage: "perp trade pnl-track", description: "Real-time PnL tracker" },
993
+ split: { usage: "perp trade split <symbol> <side> <usd>", description: "Split large order into depth-based slices" },
994
+ multi: { usage: "perp trade multi <legs...>", description: "Execute multi-leg orders (exchange:symbol:side:size)" },
995
+ "cancel-stop": { usage: "perp trade cancel-stop <symbol> <stopOrderId>", description: "Cancel a stop order" },
996
+ "cancel-twap": { usage: "perp trade cancel-twap <symbol> <twapOrderId>", description: "Cancel a TWAP order" },
1010
997
  },
1011
998
  },
1012
999
  arb: {
1013
1000
  description: "Funding rate arbitrage & basis trading",
1014
1001
  subcommands: {
1015
- rates: { usage: "perp arb scan", description: "Compare funding rates" },
1016
- scan: { usage: "perp arb scan --min <bps>", description: "Find arb opportunities" },
1017
- funding: { usage: "perp arb funding", description: "Detailed funding analysis" },
1018
- dex: { usage: "perp arb dex", description: "HIP-3 cross-dex arb" },
1019
- auto: { usage: "perp arb auto --min-spread <bps>", description: "Auto-execute funding arb" },
1002
+ scan: { usage: "perp arb scan --min <pct>", description: "Find arb opportunities (use --rates, --gaps, --hip3, --positions, --basis, --compare, --history, --live)" },
1003
+ auto: { usage: "perp arb auto --min-spread <pct>", description: "Auto-execute funding arb" },
1004
+ exec: { usage: "perp arb exec <symbol> <longEx> <shortEx> <size>", description: "Execute arb trade on two exchanges" },
1020
1005
  status: { usage: "perp arb status", description: "Current arb positions" },
1021
1006
  close: { usage: "perp arb close <symbol>", description: "Close arb position" },
1022
- history: { usage: "perp arb history", description: "Arb execution history" },
1007
+ "history|log": { usage: "perp arb history", description: "Arb execution history" },
1008
+ config: { usage: "perp arb config", description: "View/edit arb configuration" },
1009
+ rebalance: { usage: "perp arb rebalance", description: "Rebalance funds across exchanges for arb" },
1023
1010
  },
1024
1011
  },
1025
1012
  risk: {
@@ -1043,32 +1030,48 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
1043
1030
  bot: {
1044
1031
  description: "Automated trading bots",
1045
1032
  subcommands: {
1033
+ start: { usage: "perp bot start <config>", description: "Start bot from config file" },
1046
1034
  twap: { usage: "perp bot twap <symbol> <side> <size> <duration>", description: "TWAP execution" },
1047
1035
  grid: { usage: "perp bot grid <symbol> --range <pct> --grids <n> --size <usd>", description: "Grid trading bot" },
1048
1036
  dca: { usage: "perp bot dca <symbol> <side> <amount> <interval>", description: "DCA bot" },
1037
+ "funding-arb": { usage: "perp bot funding-arb", description: "Funding arb bot" },
1049
1038
  "trailing-stop": { usage: "perp bot trailing-stop <symbol>", description: "Trailing stop bot" },
1050
1039
  "quick-grid": { usage: "perp bot quick-grid <symbol>", description: "Quick grid bot" },
1051
1040
  "quick-dca": { usage: "perp bot quick-dca <symbol> <side> <amount> <interval>", description: "Quick DCA bot" },
1052
1041
  "quick-arb": { usage: "perp bot quick-arb", description: "Quick arb bot" },
1042
+ "preset-list": { usage: "perp bot preset-list", description: "List available bot presets" },
1043
+ preset: { usage: "perp bot preset <name> [symbol]", description: "Run a bot preset" },
1044
+ example: { usage: "perp bot example", description: "Show example bot config" },
1053
1045
  },
1054
1046
  },
1055
1047
  wallet: {
1056
1048
  description: "Wallet management",
1057
1049
  subcommands: {
1050
+ show: { usage: "perp wallet show", description: "Show configured wallets with public addresses" },
1058
1051
  list: { usage: "perp wallet list", description: "List wallets" },
1059
- balance: { usage: "perp wallet balance", description: "On-chain balances" },
1052
+ balance: { usage: "perp wallet balance [name]", description: "On-chain balances" },
1060
1053
  generate: { usage: "perp wallet generate solana|evm", description: "Generate new wallet" },
1054
+ import: { usage: "perp wallet import solana|evm <privateKey>", description: "Import existing private key" },
1055
+ set: { usage: "perp wallet set <exchange> <key>", description: "Set private key for exchange" },
1056
+ use: { usage: "perp wallet use <name> [exchange]", description: "Set active wallet" },
1057
+ remove: { usage: "perp wallet remove <name>", description: "Remove a wallet" },
1058
+ rename: { usage: "perp wallet rename <oldName> <newName>", description: "Rename a wallet" },
1061
1059
  },
1062
1060
  },
1063
1061
  history: {
1064
1062
  description: "Execution log, performance & audit trail",
1065
1063
  subcommands: {
1066
1064
  list: { usage: "perp history list", description: "Execution log" },
1065
+ stats: { usage: "perp history stats", description: "Execution statistics" },
1066
+ positions: { usage: "perp history positions", description: "Position history" },
1067
1067
  summary: { usage: "perp history summary", description: "Performance summary" },
1068
1068
  pnl: { usage: "perp history pnl", description: "P&L breakdown" },
1069
1069
  funding: { usage: "perp history funding", description: "Funding payment history" },
1070
1070
  report: { usage: "perp history report", description: "Full performance report" },
1071
+ snapshot: { usage: "perp history snapshot", description: "Portfolio snapshot" },
1072
+ track: { usage: "perp history track", description: "Track position over time" },
1071
1073
  perf: { usage: "perp history perf --period daily|weekly|summary", description: "Performance breakdown" },
1074
+ prune: { usage: "perp history prune", description: "Prune old execution records" },
1072
1075
  },
1073
1076
  },
1074
1077
  backtest: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.4.12",
3
+ "version": "0.5.2",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",