perp-cli 0.9.8 → 0.12.0

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 (254) hide show
  1. package/README.md +30 -25
  2. package/dist/_stderr-filter.d.ts +0 -0
  3. package/dist/_stderr-filter.js +30 -0
  4. package/dist/agent-wallet/expiry.d.ts +15 -0
  5. package/dist/agent-wallet/expiry.js +26 -0
  6. package/dist/agent-wallet/lighter-keystore.d.ts +34 -0
  7. package/dist/agent-wallet/lighter-keystore.js +232 -0
  8. package/dist/agent-wallet/passphrase.d.ts +13 -0
  9. package/dist/agent-wallet/passphrase.js +54 -0
  10. package/dist/agent-wallet/signing-strategy.d.ts +67 -0
  11. package/dist/agent-wallet/signing-strategy.js +51 -0
  12. package/dist/agent-wallet/store.d.ts +37 -0
  13. package/dist/agent-wallet/store.js +172 -0
  14. package/dist/api/public/_http.d.ts +15 -0
  15. package/dist/api/public/_http.js +24 -0
  16. package/dist/api/public/hyperliquid.js +36 -24
  17. package/dist/api/public/lighter.d.ts +7 -1
  18. package/dist/api/public/lighter.js +85 -37
  19. package/dist/api/public/pacifica.js +47 -18
  20. package/dist/bot/engine.js +23 -4
  21. package/dist/bot/presets.js +3 -3
  22. package/dist/bot/strategies/funding-arb-strategy.d.ts +0 -23
  23. package/dist/bot/strategies/funding-arb-strategy.js +41 -176
  24. package/dist/bot/strategies/funding-auto.js +2 -2
  25. package/dist/bridge-engine.js +102 -49
  26. package/dist/cli-spec.js +1 -2
  27. package/dist/commands/account.js +2 -500
  28. package/dist/commands/agent.d.ts +8 -2
  29. package/dist/commands/agent.js +1903 -366
  30. package/dist/commands/alerts.js +3 -1
  31. package/dist/commands/arb/index.js +37 -7
  32. package/dist/commands/arb-auto.js +44 -11
  33. package/dist/commands/arb.js +83 -34
  34. package/dist/commands/bot.d.ts +1 -1
  35. package/dist/commands/bot.js +20 -16
  36. package/dist/commands/bridge.d.ts +1 -1
  37. package/dist/commands/bridge.js +4 -4
  38. package/dist/commands/funds.d.ts +1 -1
  39. package/dist/commands/funds.js +18 -68
  40. package/dist/commands/jobs.d.ts +1 -1
  41. package/dist/commands/jobs.js +2 -2
  42. package/dist/commands/manage.d.ts +1 -1
  43. package/dist/commands/manage.js +12 -84
  44. package/dist/commands/plan.d.ts +1 -1
  45. package/dist/commands/plan.js +2 -2
  46. package/dist/commands/portfolio.d.ts +19 -3
  47. package/dist/commands/portfolio.js +747 -163
  48. package/dist/commands/rebalance.d.ts +1 -1
  49. package/dist/commands/rebalance.js +18 -12
  50. package/dist/commands/risk.js +2 -6
  51. package/dist/commands/settings.js +30 -53
  52. package/dist/commands/trade.js +16 -15
  53. package/dist/commands/wallet.d.ts +2 -1
  54. package/dist/commands/wallet.js +1122 -176
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +55 -9
  57. package/dist/errors.d.ts +64 -0
  58. package/dist/errors.js +82 -2
  59. package/dist/event-stream.js +12 -1
  60. package/dist/exchanges/aster-typed-data.d.ts +156 -0
  61. package/dist/exchanges/aster-typed-data.js +189 -0
  62. package/dist/exchanges/aster.d.ts +60 -11
  63. package/dist/exchanges/aster.js +278 -136
  64. package/dist/exchanges/hyperliquid.d.ts +14 -0
  65. package/dist/exchanges/hyperliquid.js +88 -34
  66. package/dist/exchanges/lighter-spot.js +19 -5
  67. package/dist/exchanges/lighter.d.ts +66 -0
  68. package/dist/exchanges/lighter.js +213 -31
  69. package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
  70. package/dist/exchanges/pacifica-typed-data.js +74 -0
  71. package/dist/exchanges/pacifica.d.ts +29 -1
  72. package/dist/exchanges/pacifica.js +94 -6
  73. package/dist/funding/rates.js +59 -21
  74. package/dist/guardrail/contracts.d.ts +14 -0
  75. package/dist/guardrail/contracts.js +72 -0
  76. package/dist/guardrail/perp-guardrail.d.ts +21 -0
  77. package/dist/guardrail/perp-guardrail.js +145 -0
  78. package/dist/index.d.ts +1 -1
  79. package/dist/index.js +307 -362
  80. package/dist/jobs.js +3 -1
  81. package/dist/mcp-server.js +92 -88
  82. package/dist/position-history.js +47 -4
  83. package/dist/settings.d.ts +69 -3
  84. package/dist/settings.js +3 -2
  85. package/dist/signer/index.d.ts +2 -0
  86. package/dist/signer/index.js +2 -0
  87. package/dist/signer/ows-evm.d.ts +32 -0
  88. package/dist/signer/ows-evm.js +86 -0
  89. package/dist/signer/ows-loader.d.ts +2 -0
  90. package/dist/signer/ows-loader.js +6 -0
  91. package/dist/signer/ows-solana.d.ts +25 -0
  92. package/dist/signer/ows-solana.js +56 -0
  93. package/dist/smart-order.d.ts +8 -2
  94. package/dist/smart-order.js +46 -11
  95. package/dist/strategies/funding-arb.js +29 -8
  96. package/dist/trade-validator.js +30 -5
  97. package/dist/utils.d.ts +10 -0
  98. package/dist/utils.js +19 -0
  99. package/package.json +4 -2
  100. package/skills/perp-cli/SKILL.md +23 -14
  101. package/skills/perp-cli/references/agent-operations.md +6 -4
  102. package/skills/perp-cli/references/commands.md +55 -34
  103. package/skills/perp-cli/scripts/preflight.sh +4 -4
  104. package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
  105. package/dist/__tests__/alert-logic.test.d.ts +0 -1
  106. package/dist/__tests__/alert-logic.test.js +0 -107
  107. package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
  108. package/dist/__tests__/arb-auto-3dex.test.js +0 -397
  109. package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
  110. package/dist/__tests__/arb-history-stats.test.js +0 -176
  111. package/dist/__tests__/arb-logic.test.d.ts +0 -1
  112. package/dist/__tests__/arb-logic.test.js +0 -84
  113. package/dist/__tests__/arb-manage.test.d.ts +0 -1
  114. package/dist/__tests__/arb-manage.test.js +0 -253
  115. package/dist/__tests__/arb-new-features.test.d.ts +0 -1
  116. package/dist/__tests__/arb-new-features.test.js +0 -457
  117. package/dist/__tests__/arb-sizing.test.d.ts +0 -1
  118. package/dist/__tests__/arb-sizing.test.js +0 -48
  119. package/dist/__tests__/arb-state.test.d.ts +0 -1
  120. package/dist/__tests__/arb-state.test.js +0 -284
  121. package/dist/__tests__/arb-userflow.test.d.ts +0 -1
  122. package/dist/__tests__/arb-userflow.test.js +0 -945
  123. package/dist/__tests__/arb-utils.test.d.ts +0 -1
  124. package/dist/__tests__/arb-utils.test.js +0 -264
  125. package/dist/__tests__/bot-conditions.test.d.ts +0 -1
  126. package/dist/__tests__/bot-conditions.test.js +0 -341
  127. package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
  128. package/dist/__tests__/client-id-tracker.test.js +0 -137
  129. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
  130. package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
  131. package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
  132. package/dist/__tests__/commands/order-intent.test.js +0 -600
  133. package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
  134. package/dist/__tests__/commands/trade-commands.test.js +0 -821
  135. package/dist/__tests__/config.test.d.ts +0 -1
  136. package/dist/__tests__/config.test.js +0 -86
  137. package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
  138. package/dist/__tests__/cross-chain-margin.test.js +0 -287
  139. package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
  140. package/dist/__tests__/dex-asset-map.test.js +0 -191
  141. package/dist/__tests__/errors.test.d.ts +0 -1
  142. package/dist/__tests__/errors.test.js +0 -110
  143. package/dist/__tests__/event-stream.test.d.ts +0 -1
  144. package/dist/__tests__/event-stream.test.js +0 -276
  145. package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
  146. package/dist/__tests__/exchanges/interface.test.js +0 -132
  147. package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
  148. package/dist/__tests__/exchanges/mock-adapter.js +0 -137
  149. package/dist/__tests__/execution-log.test.d.ts +0 -1
  150. package/dist/__tests__/execution-log.test.js +0 -106
  151. package/dist/__tests__/funding-calc.test.d.ts +0 -1
  152. package/dist/__tests__/funding-calc.test.js +0 -71
  153. package/dist/__tests__/funding-history.test.d.ts +0 -1
  154. package/dist/__tests__/funding-history.test.js +0 -343
  155. package/dist/__tests__/funding-rates.test.d.ts +0 -1
  156. package/dist/__tests__/funding-rates.test.js +0 -342
  157. package/dist/__tests__/funding.test.d.ts +0 -1
  158. package/dist/__tests__/funding.test.js +0 -173
  159. package/dist/__tests__/gap-logic.test.d.ts +0 -1
  160. package/dist/__tests__/gap-logic.test.js +0 -43
  161. package/dist/__tests__/hip3-dex.test.d.ts +0 -1
  162. package/dist/__tests__/hip3-dex.test.js +0 -234
  163. package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
  164. package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
  165. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
  166. package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
  167. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
  168. package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
  169. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
  170. package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
  171. package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
  172. package/dist/__tests__/integration/bridge.integration.test.js +0 -309
  173. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
  174. package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
  175. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
  176. package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
  177. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
  178. package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
  179. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
  180. package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
  181. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
  182. package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
  183. package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
  184. package/dist/__tests__/integration/lighter.integration.test.js +0 -53
  185. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
  186. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
  187. package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
  188. package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
  189. package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
  190. package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
  191. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
  192. package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
  193. package/dist/__tests__/liquidity.test.d.ts +0 -1
  194. package/dist/__tests__/liquidity.test.js +0 -225
  195. package/dist/__tests__/plan-executor.test.d.ts +0 -1
  196. package/dist/__tests__/plan-executor.test.js +0 -314
  197. package/dist/__tests__/position-history.test.d.ts +0 -1
  198. package/dist/__tests__/position-history.test.js +0 -367
  199. package/dist/__tests__/retry.test.d.ts +0 -1
  200. package/dist/__tests__/retry.test.js +0 -310
  201. package/dist/__tests__/risk-assessment.test.d.ts +0 -1
  202. package/dist/__tests__/risk-assessment.test.js +0 -288
  203. package/dist/__tests__/security-adversarial.test.d.ts +0 -1
  204. package/dist/__tests__/security-adversarial.test.js +0 -574
  205. package/dist/__tests__/strategies.test.d.ts +0 -1
  206. package/dist/__tests__/strategies.test.js +0 -539
  207. package/dist/__tests__/trade-execution.test.d.ts +0 -1
  208. package/dist/__tests__/trade-execution.test.js +0 -129
  209. package/dist/__tests__/trade-validator.test.d.ts +0 -1
  210. package/dist/__tests__/trade-validator.test.js +0 -655
  211. package/dist/__tests__/utils.test.d.ts +0 -1
  212. package/dist/__tests__/utils.test.js +0 -76
  213. package/dist/arb/reconcile.d.ts +0 -28
  214. package/dist/arb/reconcile.js +0 -162
  215. package/dist/commands/alert.d.ts +0 -2
  216. package/dist/commands/alert.js +0 -421
  217. package/dist/commands/analytics.d.ts +0 -3
  218. package/dist/commands/analytics.js +0 -311
  219. package/dist/commands/arb-manage.d.ts +0 -5
  220. package/dist/commands/arb-manage.js +0 -5
  221. package/dist/commands/dashboard.d.ts +0 -3
  222. package/dist/commands/dashboard.js +0 -176
  223. package/dist/commands/deposit.d.ts +0 -4
  224. package/dist/commands/deposit.js +0 -573
  225. package/dist/commands/dex.d.ts +0 -3
  226. package/dist/commands/dex.js +0 -120
  227. package/dist/commands/env.d.ts +0 -2
  228. package/dist/commands/env.js +0 -136
  229. package/dist/commands/funding.d.ts +0 -3
  230. package/dist/commands/funding.js +0 -554
  231. package/dist/commands/gap.d.ts +0 -2
  232. package/dist/commands/gap.js +0 -305
  233. package/dist/commands/health.d.ts +0 -2
  234. package/dist/commands/health.js +0 -67
  235. package/dist/commands/multileg.d.ts +0 -16
  236. package/dist/commands/multileg.js +0 -174
  237. package/dist/commands/pnl.d.ts +0 -12
  238. package/dist/commands/pnl.js +0 -243
  239. package/dist/commands/run.d.ts +0 -3
  240. package/dist/commands/run.js +0 -202
  241. package/dist/commands/stream.d.ts +0 -5
  242. package/dist/commands/stream.js +0 -200
  243. package/dist/commands/withdraw.d.ts +0 -3
  244. package/dist/commands/withdraw.js +0 -187
  245. package/dist/exchanges/edgex.d.ts +0 -62
  246. package/dist/exchanges/edgex.js +0 -552
  247. package/dist/exchanges/nado.d.ts +0 -52
  248. package/dist/exchanges/nado.js +0 -223
  249. package/dist/funding/index.d.ts +0 -3
  250. package/dist/funding/index.js +0 -3
  251. package/dist/retry.d.ts +0 -74
  252. package/dist/retry.js +0 -129
  253. package/dist/ws/market-feed.d.ts +0 -74
  254. package/dist/ws/market-feed.js +0 -227
@@ -269,7 +269,9 @@ export function registerAlertCommands(program, isJson) {
269
269
  "--cooldown", String(cooldownMin),
270
270
  ].join(" ");
271
271
  // Pass through env vars for keys
272
- const envKeys = ["LIGHTER_PRIVATE_KEY", "LIGHTER_API_KEY", "LIGHTER_ACCOUNT_INDEX", "HL_PRIVATE_KEY", "PACIFICA_PRIVATE_KEY", "PRIVATE_KEY"];
272
+ // L2 agent key + indices now live in ~/.perp keystore + settings.agents (SSOT Rule #3),
273
+ // not env. Only Tier 3 master PKs need passthrough.
274
+ const envKeys = ["LIGHTER_PRIVATE_KEY", "HL_PRIVATE_KEY", "PACIFICA_PRIVATE_KEY", "PRIVATE_KEY"];
273
275
  const envStr = envKeys.filter(k => process.env[k]).map(k => `${k}='${process.env[k]}'`).join(" ");
274
276
  const cmd = `${envStr} ${nodeCmd} ${cliPath} ${args}`;
275
277
  execSync(`tmux new-session -d -s ${session} '${cmd.replace(/'/g, "'\\''")}'`);
@@ -1,5 +1,5 @@
1
1
  import chalk from "chalk";
2
- import { makeTable, formatUsd, formatPnl, printJson, jsonOk } from "../../utils.js";
2
+ import { makeTable, formatUsd, formatPnl, printJson, jsonOk, logSettledRejections } from "../../utils.js";
3
3
  import { SPOT_PERP_TOKEN_MAP } from "../../exchanges/index.js";
4
4
  import { readExecutionLog, logExecution } from "../../execution-log.js";
5
5
  import { toHourlyRate, computeAnnualSpread } from "../../funding.js";
@@ -26,13 +26,22 @@ function formatDuration(ms) {
26
26
  }
27
27
  async function fetchFundingRatesMap() {
28
28
  const rateMap = new Map();
29
- const [pacAssets, hlAssets, ltDetails, ltFunding, asterPremiums] = await Promise.all([
29
+ // SSOT rule #2: each DEX is independent. allSettled keeps the comparison
30
+ // alive when one DEX errors out, while rejection reasons get logged to
31
+ // stderr (explicit failure, never silent .catch(() => [])).
32
+ const settled = await Promise.allSettled([
30
33
  fetchPacificaPrices(),
31
34
  fetchHyperliquidMeta(),
32
35
  fetchLighterOrderBookDetails(),
33
36
  fetchLighterFundingRates(),
34
- fetch("https://fapi.asterdex.com/fapi/v1/premiumIndex").then(r => r.json()).catch(() => []),
37
+ fetch("https://fapi.asterdex.com/fapi/v1/premiumIndex").then(r => r.json()),
35
38
  ]);
39
+ logSettledRejections(settled, ["pacifica", "hyperliquid", "lighter:orderbook", "lighter:funding", "aster:premiumIndex"], "arb");
40
+ const pacAssets = settled[0].status === "fulfilled" ? settled[0].value : [];
41
+ const hlAssets = settled[1].status === "fulfilled" ? settled[1].value : [];
42
+ const ltDetails = settled[2].status === "fulfilled" ? settled[2].value : [];
43
+ const ltFunding = settled[3].status === "fulfilled" ? settled[3].value : [];
44
+ const asterPremiums = settled[4].status === "fulfilled" ? settled[4].value : [];
36
45
  const addRate = (sym, exchange, rate, markPrice) => {
37
46
  if (!sym)
38
47
  return;
@@ -45,19 +54,40 @@ async function fetchFundingRatesMap() {
45
54
  for (const h of hlAssets)
46
55
  addRate(h.symbol, "hyperliquid", h.funding, h.markPx);
47
56
  // Lighter: join details + funding by marketId
57
+ // SSOT rule #2: skip rows whose mark price cannot be resolved (no 0 fallback).
58
+ // Prefer fr.markPrice; fall back to orderBook last-trade as documented
59
+ // price-source preference (NOT error fallback).
48
60
  const ltPriceMap = new Map(ltDetails.map(d => [d.marketId, d.lastTradePrice]));
49
61
  const ltSymMap = new Map(ltDetails.map(d => [d.marketId, d.symbol]));
50
62
  for (const fr of ltFunding) {
51
63
  const sym = fr.symbol || ltSymMap.get(fr.marketId) || "";
52
- const mp = fr.markPrice || ltPriceMap.get(fr.marketId) || 0;
64
+ if (!sym)
65
+ continue;
66
+ const directMark = fr.markPrice;
67
+ const fallbackMark = ltPriceMap.get(fr.marketId);
68
+ let mp;
69
+ if (directMark !== null && Number.isFinite(directMark) && directMark > 0) {
70
+ mp = directMark;
71
+ }
72
+ else if (fallbackMark !== undefined && Number.isFinite(fallbackMark) && fallbackMark > 0) {
73
+ mp = fallbackMark;
74
+ }
75
+ if (mp === undefined || !Number.isFinite(fr.rate))
76
+ continue;
53
77
  addRate(sym, "lighter", fr.rate, mp);
54
78
  }
55
79
  // Aster: premiumIndex
56
- for (const p of asterPremiums ?? []) {
80
+ // SSOT rule #2: skip rows missing markPrice / lastFundingRate instead of
81
+ // 0-coercing — a 0 markPrice would corrupt downstream sizing.
82
+ for (const p of asterPremiums) {
57
83
  const rawSym = String(p.symbol ?? "");
58
84
  if (!rawSym.endsWith("USDT"))
59
85
  continue;
60
- addRate(rawSym.replace(/USDT$/, ""), "aster", Number(p.lastFundingRate ?? 0), Number(p.markPrice ?? 0));
86
+ const fundingRate = Number(p.lastFundingRate);
87
+ const markPrice = Number(p.markPrice);
88
+ if (!Number.isFinite(fundingRate) || !Number.isFinite(markPrice) || markPrice <= 0)
89
+ continue;
90
+ addRate(rawSym.replace(/USDT$/, ""), "aster", fundingRate, markPrice);
61
91
  }
62
92
  return rateMap;
63
93
  }
@@ -1428,7 +1458,7 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
1428
1458
  console.log(chalk.yellow("\n [DRY RUN] No transfers executed.\n"));
1429
1459
  }
1430
1460
  else {
1431
- console.log(chalk.yellow("\n To execute, use: perp rebalance execute --auto-bridge\n"));
1461
+ console.log(chalk.yellow("\n To execute, use: perp funds rebalance execute --auto-bridge\n"));
1432
1462
  }
1433
1463
  });
1434
1464
  // ── printFundingEarned helper (used by arb status --funding) ──
@@ -1,5 +1,5 @@
1
1
  import chalk from "chalk";
2
- import { formatUsd, printJson, jsonOk, makeTable } from "../utils.js";
2
+ import { formatUsd, printJson, jsonOk, makeTable, logSettledRejections } from "../utils.js";
3
3
  import { computeExecutableSize } from "../liquidity.js";
4
4
  import { computeMatchedSize, computeSpotPerpMatchedSize } from "../arb/index.js";
5
5
  import { computeAnnualSpread, toHourlyRate } from "../funding.js";
@@ -105,17 +105,30 @@ export function isSpreadReversed(longExchange, shortExchange, snapshot) {
105
105
  return longHourly > shortHourly;
106
106
  }
107
107
  async function fetchFundingSpreads() {
108
- const [pacRes, hlRes, ltDetailsRes, ltFundingRes, astPremiums] = await Promise.all([
108
+ // SSOT rule #2: each DEX fetch is independent. allSettled lets us keep the
109
+ // multi-DEX comparison alive when one DEX errors out, while the rejection
110
+ // reasons get logged to stderr (explicit failure, never silent).
111
+ const settled = await Promise.allSettled([
109
112
  fetchPacificaPricesRaw(),
110
113
  fetchHyperliquidMetaRaw(),
111
114
  fetchLighterOrderBookDetailsRaw(),
112
115
  fetchLighterFundingRatesRaw(),
113
- fetch("https://fapi.asterdex.com/fapi/v1/premiumIndex").then(r => r.json()).catch(() => []),
116
+ fetch("https://fapi.asterdex.com/fapi/v1/premiumIndex").then(r => r.json()),
114
117
  ]);
118
+ logSettledRejections(settled, ["pacifica", "hyperliquid", "lighter:orderbook", "lighter:funding", "aster:premiumIndex"], "arb-auto");
119
+ const pacRes = settled[0].status === "fulfilled" ? settled[0].value : null;
120
+ const hlRes = settled[1].status === "fulfilled" ? settled[1].value : null;
121
+ const ltDetailsRes = settled[2].status === "fulfilled" ? settled[2].value : null;
122
+ const ltFundingRes = settled[3].status === "fulfilled" ? settled[3].value : null;
123
+ const astPremiums = settled[4].status === "fulfilled" ? settled[4].value : [];
115
124
  const { rates: pacRates, prices: pacPrices } = parsePacificaRaw(pacRes);
116
125
  const { rates: hlRates, prices: hlPrices } = parseHyperliquidMetaRaw(hlRes);
117
126
  const { rates: ltRates, prices: ltPrices } = parseLighterRaw(ltDetailsRes, ltFundingRes);
118
- // Aster: parse premiumIndex
127
+ // Aster: parse premiumIndex.
128
+ // SSOT rule #2: never silently coerce missing markPrice/lastFundingRate to 0
129
+ // — a 0 markPrice corrupts every downstream sizing calculation, and a 0
130
+ // funding rate flips the symbol off the arb radar without explanation.
131
+ // Skip the symbol entirely if either field is missing or non-finite.
119
132
  const astRates = new Map();
120
133
  const astPrices = new Map();
121
134
  for (const p of astPremiums ?? []) {
@@ -123,8 +136,15 @@ async function fetchFundingSpreads() {
123
136
  if (!rawSym.endsWith("USDT"))
124
137
  continue;
125
138
  const sym = rawSym.replace(/USDT$/, "");
126
- astRates.set(sym, Number(p.lastFundingRate ?? 0));
127
- astPrices.set(sym, Number(p.markPrice ?? 0));
139
+ const fundingRate = Number(p.lastFundingRate);
140
+ const markPrice = Number(p.markPrice);
141
+ if (!Number.isFinite(fundingRate) || !Number.isFinite(markPrice) || markPrice <= 0) {
142
+ // Aster premiumIndex omitted or returned non-numeric values for this
143
+ // symbol; skip rather than poison downstream math with a 0.
144
+ continue;
145
+ }
146
+ astRates.set(sym, fundingRate);
147
+ astPrices.set(sym, markPrice);
128
148
  }
129
149
  const snapshots = [];
130
150
  const allSymbols = new Set([...pacRates.keys(), ...hlRates.keys(), ...ltRates.keys(), ...astRates.keys()]);
@@ -246,8 +266,8 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
246
266
  console.log(chalk.green(`\n Funding arb bot started in background.`));
247
267
  console.log(` ID: ${chalk.white.bold(job.id)}`);
248
268
  console.log(` Min spread: ${opts.minSpread}% | Size: $${opts.size}`);
249
- console.log(` Logs: ${chalk.gray(`perp jobs logs ${job.id}`)}`);
250
- console.log(` Stop: ${chalk.gray(`perp jobs stop ${job.id}`)}\n`);
269
+ console.log(` Logs: ${chalk.gray(`perp background logs ${job.id}`)}`);
270
+ console.log(` Stop: ${chalk.gray(`perp background stop ${job.id}`)}\n`);
251
271
  return;
252
272
  }
253
273
  const minSpread = parseFloat(opts.minSpread);
@@ -1470,10 +1490,23 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1470
1490
  const shortOk = shortResult.status === "fulfilled";
1471
1491
  if (longOk && shortOk) {
1472
1492
  // 6. Verify both positions actually exist (some exchanges return success but silently reject)
1473
- const [longPositions, shortPositions] = await Promise.all([
1474
- longAdapter.getPositions().catch(() => []),
1475
- shortAdapter.getPositions().catch(() => []),
1493
+ // SSOT rule #2: distinguish "API error" from "no position opened".
1494
+ // Previously `.catch(() => [])` collapsed both into the same code path,
1495
+ // so a transient verify-side network blip would be misclassified as a
1496
+ // silent exchange reject and trigger an unnecessary rollback.
1497
+ const [longSettled, shortSettled] = await Promise.allSettled([
1498
+ longAdapter.getPositions(),
1499
+ shortAdapter.getPositions(),
1476
1500
  ]);
1501
+ if (longSettled.status === "rejected" || shortSettled.status === "rejected") {
1502
+ const longErr = longSettled.status === "rejected" ? (longSettled.reason instanceof Error ? longSettled.reason.message : String(longSettled.reason)) : "ok";
1503
+ const shortErr = shortSettled.status === "rejected" ? (shortSettled.reason instanceof Error ? shortSettled.reason.message : String(shortSettled.reason)) : "ok";
1504
+ throw new Error(`arb-auto: cannot verify post-fill positions for ${sym} ` +
1505
+ `(long ${longExch}: ${longErr}; short ${shortExch}: ${shortErr}). ` +
1506
+ `Refusing to silently misclassify API failure as a silent reject.`);
1507
+ }
1508
+ const longPositions = longSettled.value;
1509
+ const shortPositions = shortSettled.value;
1477
1510
  const longPos = longPositions.find(p => p.symbol.toUpperCase().startsWith(sym));
1478
1511
  const shortPos = shortPositions.find(p => p.symbol.toUpperCase().startsWith(sym));
1479
1512
  if (!longPos && !shortPos) {
@@ -1,4 +1,4 @@
1
- import { makeTable, formatUsd, formatPercent, formatPnl, printJson, jsonOk, withJsonErrors } from "../utils.js";
1
+ import { makeTable, formatUsd, formatPercent, formatPnl, printJson, jsonOk, withJsonErrors, logSettledRejections } from "../utils.js";
2
2
  import chalk from "chalk";
3
3
  import { annualizeRate, toHourlyRate, estimateHourlyFunding, annualizeHourlyRate } from "../funding.js";
4
4
  import { fetchAllFundingRates, fetchSymbolFundingRates, TOP_SYMBOLS, } from "../funding-rates.js";
@@ -25,47 +25,76 @@ async function fetchHyperliquidRates() {
25
25
  }));
26
26
  }
27
27
  async function fetchAsterRates() {
28
+ // SSOT rule #2: do NOT silently coerce missing markPrice / lastFundingRate
29
+ // to 0 — that would corrupt comparisons against other DEXs. Skip symbols
30
+ // whose Aster row is malformed instead. The outer try/catch is intentional:
31
+ // if the entire endpoint fails, the arb command can still compare the
32
+ // remaining DEXs (graceful degradation is an explicit caller-facing
33
+ // behavior, not a silent data substitution).
28
34
  try {
29
35
  const premiums = await fetch("https://fapi.asterdex.com/fapi/v1/premiumIndex").then(r => r.json());
30
- return (premiums ?? [])
31
- .filter(p => String(p.symbol ?? "").endsWith("USDT"))
32
- .map(p => ({
33
- exchange: "aster",
34
- symbol: String(p.symbol ?? "").replace(/USDT$/, ""),
35
- fundingRate: Number(p.lastFundingRate ?? 0),
36
- markPrice: Number(p.markPrice ?? 0),
37
- nextFunding: Number(p.nextFundingTime ?? 0),
38
- }));
36
+ const out = [];
37
+ for (const p of premiums ?? []) {
38
+ const rawSym = String(p.symbol ?? "");
39
+ if (!rawSym.endsWith("USDT"))
40
+ continue;
41
+ const fundingRate = Number(p.lastFundingRate);
42
+ const markPrice = Number(p.markPrice);
43
+ const nextFunding = Number(p.nextFundingTime);
44
+ if (!Number.isFinite(fundingRate) || !Number.isFinite(markPrice) || markPrice <= 0) {
45
+ continue;
46
+ }
47
+ out.push({
48
+ exchange: "aster",
49
+ symbol: rawSym.replace(/USDT$/, ""),
50
+ fundingRate,
51
+ markPrice,
52
+ nextFunding: Number.isFinite(nextFunding) ? nextFunding : 0,
53
+ });
54
+ }
55
+ return out;
39
56
  }
40
57
  catch {
41
58
  return [];
42
59
  }
43
60
  }
44
61
  async function fetchLighterRates() {
45
- try {
46
- const [details, funding] = await Promise.all([
47
- fetchLighterOrderBookDetails(),
48
- fetchLighterFundingRates(),
49
- ]);
50
- const priceMap = new Map(details.map(d => [d.marketId, d.lastTradePrice]));
51
- const symMap = new Map(details.map(d => [d.marketId, d.symbol]));
52
- const rates = [];
53
- for (const fr of funding) {
54
- const symbol = fr.symbol || symMap.get(fr.marketId);
55
- if (!symbol)
56
- continue;
57
- rates.push({
58
- exchange: "lighter",
59
- symbol,
60
- fundingRate: fr.rate,
61
- markPrice: fr.markPrice || priceMap.get(fr.marketId) || 0,
62
- });
62
+ // SSOT rule #2: error must propagate. Caller (handleBasisScan) uses
63
+ // Promise.allSettled to keep multi-DEX comparison alive when one DEX is
64
+ // down, with explicit stderr logging.
65
+ const [details, funding] = await Promise.all([
66
+ fetchLighterOrderBookDetails(),
67
+ fetchLighterFundingRates(),
68
+ ]);
69
+ const priceMap = new Map(details.map(d => [d.marketId, d.lastTradePrice]));
70
+ const symMap = new Map(details.map(d => [d.marketId, d.symbol]));
71
+ const rates = [];
72
+ for (const fr of funding) {
73
+ const symbol = fr.symbol || symMap.get(fr.marketId);
74
+ if (!symbol)
75
+ continue;
76
+ // Prefer fr.markPrice; fall back to orderBook last-trade as documented
77
+ // price-source preference (NOT error fallback). Skip if neither resolves
78
+ // — never publish a 0 markPrice downstream.
79
+ const directMark = fr.markPrice;
80
+ const fallbackMark = priceMap.get(fr.marketId);
81
+ let markPrice;
82
+ if (directMark !== null && Number.isFinite(directMark) && directMark > 0) {
83
+ markPrice = directMark;
63
84
  }
64
- return rates;
65
- }
66
- catch {
67
- return [];
85
+ else if (fallbackMark !== undefined && Number.isFinite(fallbackMark) && fallbackMark > 0) {
86
+ markPrice = fallbackMark;
87
+ }
88
+ if (markPrice === undefined || !Number.isFinite(fr.rate))
89
+ continue;
90
+ rates.push({
91
+ exchange: "lighter",
92
+ symbol,
93
+ fundingRate: fr.rate,
94
+ markPrice,
95
+ });
68
96
  }
97
+ return rates;
69
98
  }
70
99
  // annualize moved to ../funding.ts — use annualizeRate() / computeAnnualSpread()
71
100
  export function registerArbCommands(program, isJson, getAdapterForExchange) {
@@ -74,11 +103,18 @@ export function registerArbCommands(program, isJson, getAdapterForExchange) {
74
103
  // → consolidated into 'arb scan' (in arb-auto.ts) with routing options
75
104
  }
76
105
  export async function fetchAllPrices() {
77
- const [pacRes, hlRes, ltRes] = await Promise.all([
106
+ // SSOT rule #2: each DEX is independent. allSettled keeps the comparison
107
+ // alive when one DEX errors out, while rejection reasons get logged to
108
+ // stderr (explicit failure, never silent).
109
+ const settled = await Promise.allSettled([
78
110
  fetchPacificaPricesRaw(),
79
111
  fetchHyperliquidAllMidsRaw(),
80
112
  fetchLighterOrderBookDetailsRaw(),
81
113
  ]);
114
+ logSettledRejections(settled, ["pacifica", "hyperliquid", "lighter:orderbook"], "arb");
115
+ const pacRes = settled[0].status === "fulfilled" ? settled[0].value : null;
116
+ const hlRes = settled[1].status === "fulfilled" ? settled[1].value : null;
117
+ const ltRes = settled[2].status === "fulfilled" ? settled[2].value : null;
82
118
  const { prices: pacPrices } = parsePacificaRaw(pacRes);
83
119
  const hlPrices = new Map();
84
120
  if (hlRes && typeof hlRes === "object" && !Array.isArray(hlRes)) {
@@ -320,7 +356,20 @@ export async function handleRates(isJson, opts) {
320
356
  export async function handleBasisScan(isJson, opts) {
321
357
  if (!isJson())
322
358
  console.log(chalk.cyan(" Fetching prices for basis calculation...\n"));
323
- const [pacRates, hlRates, ltRates, astRates] = await Promise.all([fetchPacificaRates(), fetchHyperliquidRates(), fetchLighterRates(), fetchAsterRates()]);
359
+ // SSOT rule #2: each DEX is independent. allSettled keeps the comparison
360
+ // alive when one DEX errors out, while rejection reasons get logged to
361
+ // stderr (explicit failure, never silent [] downgrade).
362
+ const settled = await Promise.allSettled([
363
+ fetchPacificaRates(),
364
+ fetchHyperliquidRates(),
365
+ fetchLighterRates(),
366
+ fetchAsterRates(),
367
+ ]);
368
+ logSettledRejections(settled, ["pacifica", "hyperliquid", "lighter", "aster"], "basis-scan");
369
+ const pacRates = settled[0].status === "fulfilled" ? settled[0].value : [];
370
+ const hlRates = settled[1].status === "fulfilled" ? settled[1].value : [];
371
+ const ltRates = settled[2].status === "fulfilled" ? settled[2].value : [];
372
+ const astRates = settled[3].status === "fulfilled" ? settled[3].value : [];
324
373
  const exchangePrices = new Map();
325
374
  const filterSymbol = opts.symbol?.toUpperCase();
326
375
  for (const r of [...pacRates, ...hlRates, ...ltRates, ...astRates]) {
@@ -1,3 +1,3 @@
1
1
  import { Command } from "commander";
2
2
  import type { ExchangeAdapter } from "../exchanges/index.js";
3
- export declare function registerBotCommands(program: Command, getAdapter: () => Promise<ExchangeAdapter>, getAdapterFor: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean): void;
3
+ export declare function registerStrategyCommands(program: Command, getAdapter: () => Promise<ExchangeAdapter>, getAdapterFor: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean): void;
@@ -3,8 +3,9 @@ import { printJson, jsonOk, jsonError } from "../utils.js";
3
3
  import { loadBotConfig, parseStrategy, quickGridConfig, quickDCAConfig, runBot, PRESETS, getPreset, getPresetsByStrategy } from "../bot/index.js";
4
4
  import { updateJobState } from "../jobs.js";
5
5
  import { runTWAP, runFundingArb, runGrid, runDCA, runTrailingStop } from "../strategies/index.js";
6
- export function registerBotCommands(program, getAdapter, getAdapterFor, isJson) {
7
- const bot = program.command("bot").description("Automated trading bots with condition monitoring & risk management");
6
+ import { registerStrategyPlanCommands } from "./plan.js";
7
+ export function registerStrategyCommands(program, getAdapter, getAdapterFor, isJson) {
8
+ const bot = program.command("strategy").description("Trading strategies — long-running bot algorithms + one-shot scripted plans");
8
9
  // ── bot start <config> ──
9
10
  bot
10
11
  .command("start <config>")
@@ -165,7 +166,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
165
166
  const config = {
166
167
  name: `arb-funding-${Date.now().toString(36)}`,
167
168
  exchange: primaryAdapter.name,
168
- symbol: "ETH", // multi-symbol scanning
169
+ symbol: "ALL", // multi-symbol scanning
169
170
  strategy: {
170
171
  type: "funding-arb",
171
172
  min_spread: parseFloat(opts.minSpread),
@@ -243,7 +244,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
243
244
  const config = {
244
245
  name: `delta-neutral-${Date.now().toString(36)}`,
245
246
  exchange: primaryAdapter.name,
246
- symbol: "ETH",
247
+ symbol: "ALL",
247
248
  strategy: {
248
249
  type: "spot-perp-arb",
249
250
  exchanges: exchangeNames,
@@ -312,9 +313,9 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
312
313
  }
313
314
  console.log();
314
315
  }
315
- console.log(chalk.gray(` Usage: perp bot preset <name> <symbol>`));
316
- console.log(chalk.gray(` perp bot preset grid-standard ETH`));
317
- console.log(chalk.gray(` perp bot preset arb-conservative --background\n`));
316
+ console.log(chalk.gray(` Usage: perp strategy preset <name> <symbol>`));
317
+ console.log(chalk.gray(` perp strategy preset grid-standard ETH`));
318
+ console.log(chalk.gray(` perp strategy preset arb-conservative --background\n`));
318
319
  });
319
320
  // ── bot preset <name> <symbol> ──
320
321
  bot
@@ -327,7 +328,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
327
328
  const preset = getPreset(name);
328
329
  if (!preset) {
329
330
  console.error(chalk.red(`\n Unknown preset: "${name}"`));
330
- console.error(chalk.gray(` Run 'perp bot preset-list' to see available presets.\n`));
331
+ console.error(chalk.gray(` Run 'perp strategy preset-list' to see available presets.\n`));
331
332
  return;
332
333
  }
333
334
  const sym = symbol?.toUpperCase() ?? "ETH";
@@ -401,6 +402,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
401
402
  // ── bot list-strategies ──
402
403
  bot
403
404
  .command("list-strategies")
405
+ .alias("list")
404
406
  .description("List all available trading strategies")
405
407
  .action(async () => {
406
408
  await import("../bot/engine.js");
@@ -510,7 +512,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
510
512
  // ── bot run <strategy> <symbol> ──
511
513
  bot
512
514
  .command("run [strategy] [symbol]")
513
- .description("Run a strategy (use 'perp bot list-strategies' to see all). Symbol optional for multi-symbol strategies.")
515
+ .description("Run a strategy (use 'perp strategy list-strategies' to see all). Symbol optional for multi-symbol strategies.")
514
516
  .option("--config <path>", "YAML/JSON config file")
515
517
  .option("--headless", "Run without TUI dashboard")
516
518
  .option("--param <key=value>", "Strategy parameter (repeatable)", (val, acc) => [...acc, val], [])
@@ -531,7 +533,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
531
533
  console.log(chalk.cyan.bold("\n Available Strategies:\n"));
532
534
  for (const s of available)
533
535
  console.log(` ${chalk.green(s)}`);
534
- console.log(chalk.gray(`\n Usage: perp bot run <strategy> [symbol]\n`));
536
+ console.log(chalk.gray(`\n Usage: perp strategy run <strategy> [symbol]\n`));
535
537
  return;
536
538
  }
537
539
  const sym = symbol?.toUpperCase() || "ALL";
@@ -547,7 +549,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
547
549
  return process.exit(1);
548
550
  }
549
551
  console.error(chalk.red(`\n Error: The '${strategyName}' strategy requires a <symbol> argument.`));
550
- console.error(chalk.gray(` Usage: perp bot run ${strategyName} <symbol>\n`));
552
+ console.error(chalk.gray(` Usage: perp strategy run ${strategyName} <symbol>\n`));
551
553
  return process.exit(1);
552
554
  }
553
555
  // Load or build config
@@ -656,7 +658,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
656
658
  { name: "eth-dca-dip", type: "dca", file: "~/.perp/bots/eth-dca.yaml", config: { exchange: "hyperliquid", symbol: "ETH", strategy: { type: "dca", amount: 0.01, interval_sec: 3600, total_orders: 24 }, entry_conditions: [{ type: "price_below", value: 2500 }], exit_conditions: [{ type: "price_above", value: 2800 }], risk: { max_drawdown: 100, max_daily_loss: 50 } } },
657
659
  { name: "funding-arb", type: "funding-arb", file: "~/.perp/bots/funding-arb.yaml", config: { exchange: "hyperliquid", symbol: "ETH", strategy: { type: "funding-arb", min_spread: 20, close_spread: 5, size_usd: 100, max_positions: 3, exchanges: ["pacifica", "hyperliquid"] }, entry_conditions: [{ type: "always", value: 0 }], risk: { max_drawdown: 200, max_daily_loss: 50 } } },
658
660
  ],
659
- usage: ["perp bot start ~/.perp/bots/eth-grid.yaml", "perp bot start ~/.perp/bots/eth-grid.yaml --background"],
661
+ usage: ["perp strategy start ~/.perp/bots/eth-grid.yaml", "perp strategy start ~/.perp/bots/eth-grid.yaml --background"],
660
662
  }));
661
663
  return;
662
664
  }
@@ -670,11 +672,13 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
670
672
  console.log(chalk.white.bold(" 3. Funding Arb Bot"));
671
673
  console.log(chalk.gray(` Save to ~/.perp/bots/funding-arb.yaml:\n`));
672
674
  console.log(`${ARB_EXAMPLE}\n`);
673
- console.log(chalk.gray(` Usage: perp bot start ~/.perp/bots/eth-grid.yaml`));
674
- console.log(chalk.gray(` perp bot start ~/.perp/bots/eth-grid.yaml --background\n`));
675
+ console.log(chalk.gray(` Usage: perp strategy start ~/.perp/bots/eth-grid.yaml`));
676
+ console.log(chalk.gray(` perp strategy start ~/.perp/bots/eth-grid.yaml --background\n`));
675
677
  });
676
678
  // Strategy subcommands: twap, funding-arb, grid, dca, trailing-stop
677
679
  registerRunSubcommands(bot, getAdapter, getAdapterFor, isJson);
680
+ // Nested scripted plans: `perp strategy plan {validate|execute|example}`
681
+ registerStrategyPlanCommands(bot, getAdapter, isJson);
678
682
  }
679
683
  function makeLog() {
680
684
  return (msg) => {
@@ -692,8 +696,8 @@ function resolveOutputMode(isJson, headless) {
692
696
  function printBotJobStarted(name, jobId) {
693
697
  console.log(chalk.green(`\n Bot "${name}" started in background.`));
694
698
  console.log(` Job ID: ${chalk.white.bold(jobId)}`);
695
- console.log(` Logs: ${chalk.gray(`perp jobs logs ${jobId} -f`)}`);
696
- console.log(` Stop: ${chalk.gray(`perp jobs stop ${jobId}`)}\n`);
699
+ console.log(` Logs: ${chalk.gray(`perp background logs ${jobId} -f`)}`);
700
+ console.log(` Stop: ${chalk.gray(`perp background stop ${jobId}`)}\n`);
697
701
  }
698
702
  // ── Example YAML configs ──
699
703
  const GRID_EXAMPLE = chalk.gray(` name: eth-grid-bot
@@ -1,2 +1,2 @@
1
1
  import { Command } from "commander";
2
- export declare function registerBridgeCommands(program: Command, isJson: () => boolean): void;
2
+ export declare function registerFundsBridgeCommands(parent: Command, isJson: () => boolean): void;
@@ -2,8 +2,8 @@ import chalk from "chalk";
2
2
  import { formatUsd, makeTable, printJson, jsonOk } from "../utils.js";
3
3
  import { logExecution } from "../execution-log.js";
4
4
  import { executeBestBridge, getAllQuotes, checkDebridgeStatus, checkBridgeGasBalance, CHAIN_IDS, USDC_ADDRESSES, EXCHANGE_TO_CHAIN, } from "../bridge-engine.js";
5
- export function registerBridgeCommands(program, isJson) {
6
- const bridge = program.command("bridge").description("Cross-chain USDC bridge (deBridge DLN). See also: perp funds bridge (CCTP V2)");
5
+ export function registerFundsBridgeCommands(parent, isJson) {
6
+ const bridge = parent.command("bridge").description("Cross-chain USDC bridge (multi-provider: cctp, relay, debridge — auto-cheapest)");
7
7
  // ── bridge chains ──
8
8
  bridge
9
9
  .command("chains")
@@ -79,7 +79,7 @@ export function registerBridgeCommands(program, isJson) {
79
79
  }
80
80
  }
81
81
  console.log();
82
- console.log(chalk.gray(` Execute with: perp bridge send --from ${srcChain} --to ${dstChain} --amount ${opts.amount}`));
82
+ console.log(chalk.gray(` Execute with: perp funds bridge send --from ${srcChain} --to ${dstChain} --amount ${opts.amount}`));
83
83
  console.log(chalk.gray(` Pick provider: --provider cctp|relay|debridge\n`));
84
84
  });
85
85
  // ── bridge send ──
@@ -374,7 +374,7 @@ export function registerBridgeCommands(program, isJson) {
374
374
  console.log(chalk.green(` USDC arrived on ${dstExchange}!`));
375
375
  }
376
376
  console.log(chalk.gray(`\n After arrival, deposit into ${dstExchange}:`));
377
- console.log(chalk.gray(` perp deposit ${dstExchange} ${opts.amount}\n`));
377
+ console.log(chalk.gray(` perp funds deposit ${dstExchange} ${opts.amount}\n`));
378
378
  });
379
379
  // ── bridge status ──
380
380
  bridge
@@ -1,4 +1,4 @@
1
1
  import { Command } from "commander";
2
2
  import type { ExchangeAdapter } from "../exchanges/index.js";
3
3
  import type { Network } from "../pacifica/index.js";
4
- export declare function registerFundsCommands(program: Command, getAdapter: () => Promise<ExchangeAdapter>, isJson: () => boolean, getNetwork: () => Network): void;
4
+ export declare function registerFundsCommands(program: Command, getAdapter: () => Promise<ExchangeAdapter>, isJson: () => boolean, getNetwork: () => Network, getAdapterForExchange: (exchange: string) => Promise<ExchangeAdapter>): void;
@@ -2,6 +2,8 @@ import chalk from "chalk";
2
2
  import { printJson, formatUsd, jsonOk, jsonError } from "../utils.js";
3
3
  import { logExecution } from "../execution-log.js";
4
4
  import { hasPacificaSdk, hasEvmAddress, isWithdrawCapable, isUsdTransferCapable, hasLighterAccount } from "../exchanges/capabilities.js";
5
+ import { registerFundsBridgeCommands } from "./bridge.js";
6
+ import { registerFundsRebalanceCommands } from "./rebalance.js";
5
7
  const DEFAULT_RELAYER = "http://localhost:3100";
6
8
  function getRelayerUrl() {
7
9
  return process.env.PERP_RELAYER_URL || DEFAULT_RELAYER;
@@ -15,8 +17,8 @@ async function relayerAvailable() {
15
17
  return false;
16
18
  }
17
19
  }
18
- export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
19
- const funds = program.command("funds").description("Deposit, withdraw, bridge & transfer funds");
20
+ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, getAdapterForExchange) {
21
+ const funds = program.command("funds").description("Fund movement — deposit, withdraw, transfer, bridge, rebalance");
20
22
  // ═══════════════════════════════════════════════════════
21
23
  // DEPOSIT
22
24
  // ═══════════════════════════════════════════════════════
@@ -669,69 +671,6 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
669
671
  console.log(` To: ${destination}\n`);
670
672
  });
671
673
  // ═══════════════════════════════════════════════════════
672
- // BRIDGE (CCTP)
673
- // ═══════════════════════════════════════════════════════
674
- funds
675
- .command("bridge")
676
- .description("Bridge USDC between chains via CCTP V2. See also: perp bridge (deBridge DLN)")
677
- .requiredOption("--from <chain>", "Source chain (arbitrum, ethereum)")
678
- .requiredOption("--to <chain>", "Destination chain (arbitrum, ethereum, solana)")
679
- .requiredOption("--amount <amount>", "USDC amount")
680
- .requiredOption("--recipient <address>", "Recipient address on destination chain")
681
- .action(async (opts) => {
682
- const amountNum = parseFloat(opts.amount);
683
- if (isNaN(amountNum) || amountNum <= 0)
684
- throw new Error("Invalid amount");
685
- if (await relayerAvailable()) {
686
- if (!isJson())
687
- console.log(chalk.cyan(`\n Bridging $${formatUsd(amountNum)} USDC via CCTP V2 (${opts.from} → ${opts.to})...\n`));
688
- const res = await fetch(`${getRelayerUrl()}/bridge/cctp`, {
689
- method: "POST",
690
- headers: { "Content-Type": "application/json" },
691
- body: JSON.stringify({
692
- fromChain: opts.from,
693
- toChain: opts.to,
694
- amount: amountNum,
695
- recipient: opts.recipient,
696
- }),
697
- });
698
- const result = (await res.json());
699
- if (!res.ok)
700
- throw new Error(String(result.error || "CCTP bridge failed"));
701
- if (isJson())
702
- return printJson(jsonOk(result));
703
- console.log(chalk.green(` Burn TX submitted!`));
704
- console.log(` TX Hash: ${result.txHash}`);
705
- console.log(` Message Hash: ${result.messageHash}`);
706
- console.log(chalk.gray(`\n Waiting for Circle attestation (~1-3 min)...`));
707
- console.log(chalk.gray(` Check: perp funds bridge-status --hash ${result.messageHash}\n`));
708
- }
709
- else {
710
- if (isJson()) {
711
- printJson(jsonError("RELAYER_UNAVAILABLE", "CCTP bridge requires relayer server. Start: cd packages/relayer && pnpm start"));
712
- process.exit(1);
713
- }
714
- console.error(chalk.red("\n CCTP bridge requires relayer server."));
715
- console.error(chalk.gray(" Start: cd packages/relayer && pnpm start\n"));
716
- process.exit(1);
717
- }
718
- });
719
- funds
720
- .command("bridge-status")
721
- .description("Check CCTP bridge status")
722
- .requiredOption("--hash <messageHash>", "Message hash from bridge TX")
723
- .action(async (opts) => {
724
- const res = await fetch(`${getRelayerUrl()}/bridge/cctp/status/${opts.hash}`);
725
- const result = (await res.json());
726
- if (isJson())
727
- return printJson(jsonOk(result));
728
- console.log(chalk.cyan.bold("\n CCTP Bridge Status\n"));
729
- console.log(` Status: ${result.status === "complete" ? chalk.green("complete") : chalk.yellow(String(result.status))}`);
730
- if (result.attestation)
731
- console.log(` Attestation: ${chalk.gray("received")}`);
732
- console.log();
733
- });
734
- // ═══════════════════════════════════════════════════════
735
674
  // INFO (combined deposit + withdraw)
736
675
  // ═══════════════════════════════════════════════════════
737
676
  funds
@@ -791,9 +730,20 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
791
730
  console.log(chalk.white.bold(" Internal Transfer") + chalk.gray(" (Hyperliquid → Hyperliquid)"));
792
731
  console.log(` Speed: instant | Fee: none`);
793
732
  console.log(` Command: ${chalk.green("perp -e hl funds transfer <amount> <address>")}`);
794
- console.log(chalk.white.bold("\n CCTP Bridge") + chalk.gray(" (Cross-chain USDC)"));
795
- console.log(` Routes: Arbitrum ↔ Ethereum ↔ Solana`);
796
- console.log(` Command: ${chalk.green("perp funds bridge --from arbitrum --to solana --amount 100 --recipient <addr>")}`);
733
+ console.log(chalk.white.bold("\n Cross-chain Bridge") + chalk.gray(" (multi-provider: cctp, relay, debridge)"));
734
+ console.log(` Routes: Solana, Arbitrum, Base, Avalanche, Ethereum`);
735
+ console.log(` Quote: ${chalk.green("perp funds bridge quote --from <chain> --to <chain> --amount <n>")}`);
736
+ console.log(` Send: ${chalk.green("perp funds bridge send --from <chain> --to <chain> --amount <n>")}`);
737
+ console.log(` Between exchanges: ${chalk.green("perp funds bridge exchange --from <ex> --to <ex> --amount <n>")}`);
738
+ console.log(chalk.white.bold("\n Inter-exchange Rebalance") + chalk.gray(" (orchestrated withdraw → bridge → deposit)"));
739
+ console.log(` Check: ${chalk.green("perp funds rebalance check")}`);
740
+ console.log(` Plan: ${chalk.green("perp funds rebalance plan")}`);
741
+ console.log(` Execute: ${chalk.green("perp funds rebalance execute --auto-bridge")}`);
797
742
  console.log(chalk.gray("\n Use --no-relay to skip relayer and pay gas yourself.\n"));
798
743
  });
744
+ // ═══════════════════════════════════════════════════════
745
+ // NESTED SUBTREES — bridge (multi-provider) + rebalance
746
+ // ═══════════════════════════════════════════════════════
747
+ registerFundsBridgeCommands(funds, isJson);
748
+ registerFundsRebalanceCommands(funds, getAdapterForExchange, isJson);
799
749
  }