perp-cli 0.14.2 → 0.15.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 (313) hide show
  1. package/README.md +152 -13
  2. package/dist/agent-wallet/store.d.ts +3 -2
  3. package/dist/agent-wallet/store.js +2 -2
  4. package/dist/api/public/hyperliquid.js +4 -0
  5. package/dist/api/public/pacifica.js +83 -13
  6. package/dist/api/public/urls.d.ts +2 -0
  7. package/dist/api/public/urls.js +3 -0
  8. package/dist/arb/utils.d.ts +1 -2
  9. package/dist/arb/utils.js +1 -2
  10. package/dist/bot/apex/pulse.js +3 -0
  11. package/dist/bot/apex/radar.js +2 -1
  12. package/dist/bot/engine.d.ts +10 -1
  13. package/dist/bot/engine.js +14 -6
  14. package/dist/bot/indicators.d.ts +7 -0
  15. package/dist/bot/indicators.js +16 -0
  16. package/dist/bot/perp-order.d.ts +4 -4
  17. package/dist/bot/perp-order.js +5 -5
  18. package/dist/bot/quarantine.d.ts +1 -1
  19. package/dist/bot/quarantine.js +1 -1
  20. package/dist/bot/strategies/apex-strategy.js +3 -2
  21. package/dist/bot/strategies/engine-mm.js +6 -5
  22. package/dist/bot/strategies/funding-arb-strategy.js +9 -9
  23. package/dist/bot/strategies/funding-arb-utils.d.ts +15 -2
  24. package/dist/bot/strategies/funding-arb-utils.js +43 -24
  25. package/dist/bot/strategies/funding-auto.js +2 -2
  26. package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
  27. package/dist/bot/strategies/liquidation-mm.js +8 -2
  28. package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
  29. package/dist/bot/strategies/momentum-breakout.js +11 -3
  30. package/dist/bot/strategies/spot-perp-arb-strategy.js +23 -12
  31. package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
  32. package/dist/bot/strategies/spot-perp-executor.js +1 -1
  33. package/dist/bot/strategy-types.d.ts +2 -1
  34. package/dist/bot/tui/BotDashboard.d.ts +1 -1
  35. package/dist/bot/tui/BotDashboard.js +1 -1
  36. package/dist/cache.js +12 -6
  37. package/dist/cli-program.d.ts +15 -0
  38. package/dist/cli-program.js +125 -5
  39. package/dist/commands/agent-import.d.ts +7 -0
  40. package/dist/commands/agent-import.js +30 -1
  41. package/dist/commands/agent.d.ts +6 -3
  42. package/dist/commands/agent.js +160 -12
  43. package/dist/commands/arb/index.js +4 -4
  44. package/dist/commands/arb-auto.js +21 -17
  45. package/dist/commands/arb.d.ts +12 -0
  46. package/dist/commands/arb.js +61 -143
  47. package/dist/commands/bot.js +1 -1
  48. package/dist/commands/builder.js +1 -1
  49. package/dist/commands/funds.js +113 -3
  50. package/dist/commands/init.d.ts +1 -1
  51. package/dist/commands/init.js +27 -7
  52. package/dist/commands/manage.js +23 -21
  53. package/dist/commands/market.js +168 -36
  54. package/dist/commands/options.d.ts +9 -0
  55. package/dist/commands/options.js +134 -0
  56. package/dist/commands/pacifica-earn.d.ts +5 -0
  57. package/dist/commands/pacifica-earn.js +468 -0
  58. package/dist/commands/pacifica-orders.d.ts +14 -0
  59. package/dist/commands/pacifica-orders.js +142 -0
  60. package/dist/commands/portfolio.js +32 -47
  61. package/dist/commands/rebalance.d.ts +1 -0
  62. package/dist/commands/rebalance.js +15 -0
  63. package/dist/commands/risk.d.ts +11 -2
  64. package/dist/commands/risk.js +39 -44
  65. package/dist/commands/trade.d.ts +1 -0
  66. package/dist/commands/trade.js +63 -58
  67. package/dist/commands/wallet.js +1 -1
  68. package/dist/config.d.ts +2 -2
  69. package/dist/config.js +15 -4
  70. package/dist/constants.js +6 -0
  71. package/dist/dashboard/server.js +1 -1
  72. package/dist/dashboard/ui.js +1 -1
  73. package/dist/errors.d.ts +24 -1
  74. package/dist/errors.js +25 -3
  75. package/dist/exchanges/arcus/adapter.d.ts +7 -7
  76. package/dist/exchanges/arcus/adapter.js +12 -12
  77. package/dist/exchanges/arcus/agent.d.ts +1 -1
  78. package/dist/exchanges/arcus/agent.js +1 -1
  79. package/dist/exchanges/arcus/client.js +1 -1
  80. package/dist/exchanges/arcus/signing.d.ts +2 -3
  81. package/dist/exchanges/arcus/signing.js +1 -1
  82. package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
  83. package/dist/exchanges/arcus/spot-chain.js +1 -1
  84. package/dist/exchanges/arcus/spot-router.d.ts +1 -1
  85. package/dist/exchanges/arcus/spot-router.js +2 -2
  86. package/dist/exchanges/arcus/spot.d.ts +2 -3
  87. package/dist/exchanges/arcus/spot.js +2 -3
  88. package/dist/exchanges/aster-spot.d.ts +3 -3
  89. package/dist/exchanges/aster-spot.js +6 -9
  90. package/dist/exchanges/aster-typed-data.d.ts +1 -1
  91. package/dist/exchanges/aster-typed-data.js +1 -1
  92. package/dist/exchanges/aster.d.ts +9 -4
  93. package/dist/exchanges/aster.js +125 -55
  94. package/dist/exchanges/builder-fee.d.ts +1 -1
  95. package/dist/exchanges/builder-fee.js +13 -2
  96. package/dist/exchanges/canonical-symbols.d.ts +1 -2
  97. package/dist/exchanges/canonical-symbols.js +26 -2
  98. package/dist/exchanges/derive/client.d.ts +37 -0
  99. package/dist/exchanges/derive/client.js +135 -0
  100. package/dist/exchanges/derive/options.d.ts +68 -0
  101. package/dist/exchanges/derive/options.js +360 -0
  102. package/dist/exchanges/extended/adapter.d.ts +82 -0
  103. package/dist/exchanges/extended/adapter.js +179 -0
  104. package/dist/exchanges/extended/client.d.ts +38 -0
  105. package/dist/exchanges/extended/client.js +88 -0
  106. package/dist/exchanges/extended/public.d.ts +130 -0
  107. package/dist/exchanges/extended/public.js +429 -0
  108. package/dist/exchanges/grvt/adapter.d.ts +86 -0
  109. package/dist/exchanges/grvt/adapter.js +206 -0
  110. package/dist/exchanges/grvt/client.d.ts +25 -0
  111. package/dist/exchanges/grvt/client.js +86 -0
  112. package/dist/exchanges/grvt/public.d.ts +151 -0
  113. package/dist/exchanges/grvt/public.js +438 -0
  114. package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
  115. package/dist/exchanges/hyperliquid-spot.js +5 -5
  116. package/dist/exchanges/hyperliquid.d.ts +22 -16
  117. package/dist/exchanges/hyperliquid.js +80 -55
  118. package/dist/exchanges/index.d.ts +1 -1
  119. package/dist/exchanges/index.js +1 -1
  120. package/dist/exchanges/interface.d.ts +97 -8
  121. package/dist/exchanges/interface.js +19 -2
  122. package/dist/exchanges/ioc-outcome.d.ts +1 -1
  123. package/dist/exchanges/ioc-outcome.js +1 -1
  124. package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
  125. package/dist/exchanges/legacy-trading-v2.js +7 -2
  126. package/dist/exchanges/lighter-public.d.ts +2 -2
  127. package/dist/exchanges/lighter-public.js +2 -2
  128. package/dist/exchanges/lighter-signer.d.ts +1 -1
  129. package/dist/exchanges/lighter-signer.js +1 -1
  130. package/dist/exchanges/lighter-spot.d.ts +3 -4
  131. package/dist/exchanges/lighter-spot.js +6 -7
  132. package/dist/exchanges/lighter.d.ts +11 -11
  133. package/dist/exchanges/lighter.js +28 -14
  134. package/dist/exchanges/ondo/adapter.d.ts +5 -6
  135. package/dist/exchanges/ondo/adapter.js +16 -14
  136. package/dist/exchanges/ondo/agent.d.ts +1 -1
  137. package/dist/exchanges/ondo/agent.js +1 -1
  138. package/dist/exchanges/ondo/client.d.ts +1 -1
  139. package/dist/exchanges/ondo/client.js +2 -2
  140. package/dist/exchanges/ondo/public.d.ts +8 -1
  141. package/dist/exchanges/ondo/public.js +13 -2
  142. package/dist/exchanges/ondo/spot.d.ts +2 -2
  143. package/dist/exchanges/ondo/spot.js +3 -3
  144. package/dist/exchanges/options-interface.d.ts +158 -0
  145. package/dist/exchanges/options-interface.js +17 -0
  146. package/dist/exchanges/options-venues.d.ts +12 -0
  147. package/dist/exchanges/options-venues.js +35 -0
  148. package/dist/exchanges/pacifica-atomic.d.ts +211 -0
  149. package/dist/exchanges/pacifica-atomic.js +440 -0
  150. package/dist/exchanges/pacifica-decimal.d.ts +10 -0
  151. package/dist/exchanges/pacifica-decimal.js +51 -0
  152. package/dist/exchanges/pacifica-presigned.d.ts +53 -0
  153. package/dist/exchanges/pacifica-presigned.js +141 -0
  154. package/dist/exchanges/pacifica-print.d.ts +200 -0
  155. package/dist/exchanges/pacifica-print.js +417 -0
  156. package/dist/exchanges/pacifica-spot.d.ts +2 -2
  157. package/dist/exchanges/pacifica-spot.js +3 -3
  158. package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
  159. package/dist/exchanges/pacifica-typed-data.js +1 -1
  160. package/dist/exchanges/pacifica-vaults.d.ts +121 -0
  161. package/dist/exchanges/pacifica-vaults.js +264 -0
  162. package/dist/exchanges/pacifica.d.ts +32 -5
  163. package/dist/exchanges/pacifica.js +114 -48
  164. package/dist/exchanges/precision.d.ts +3 -1
  165. package/dist/exchanges/precision.js +9 -2
  166. package/dist/exchanges/qfex/adapter.d.ts +18 -5
  167. package/dist/exchanges/qfex/adapter.js +32 -12
  168. package/dist/exchanges/qfex/agent.d.ts +1 -1
  169. package/dist/exchanges/qfex/agent.js +1 -1
  170. package/dist/exchanges/qfex/client.d.ts +1 -1
  171. package/dist/exchanges/qfex/client.js +1 -1
  172. package/dist/exchanges/qfex/errors.d.ts +1 -1
  173. package/dist/exchanges/qfex/errors.js +2 -2
  174. package/dist/exchanges/qfex/public.d.ts +3 -3
  175. package/dist/exchanges/qfex/public.js +3 -3
  176. package/dist/exchanges/qfex/signing.d.ts +2 -2
  177. package/dist/exchanges/qfex/signing.js +4 -5
  178. package/dist/exchanges/qfex/ws-transport.d.ts +2 -2
  179. package/dist/exchanges/qfex/ws-transport.js +3 -3
  180. package/dist/exchanges/rate-limiter.d.ts +1 -1
  181. package/dist/exchanges/rate-limiter.js +5 -2
  182. package/dist/exchanges/registry.d.ts +1 -1
  183. package/dist/exchanges/registry.js +1 -1
  184. package/dist/exchanges/risex/adapter.d.ts +148 -0
  185. package/dist/exchanges/risex/adapter.js +796 -0
  186. package/dist/exchanges/risex/agent.d.ts +154 -0
  187. package/dist/exchanges/risex/agent.js +384 -0
  188. package/dist/exchanges/risex/client.d.ts +47 -0
  189. package/dist/exchanges/risex/client.js +174 -0
  190. package/dist/exchanges/risex/nonce.d.ts +43 -0
  191. package/dist/exchanges/risex/nonce.js +212 -0
  192. package/dist/exchanges/risex/public.d.ts +64 -0
  193. package/dist/exchanges/risex/public.js +150 -0
  194. package/dist/exchanges/risex/session.d.ts +71 -0
  195. package/dist/exchanges/risex/session.js +171 -0
  196. package/dist/exchanges/risex/typed-data.d.ts +173 -0
  197. package/dist/exchanges/risex/typed-data.js +240 -0
  198. package/dist/exchanges/risex/units.d.ts +29 -0
  199. package/dist/exchanges/risex/units.js +104 -0
  200. package/dist/exchanges/slippage.d.ts +1 -1
  201. package/dist/exchanges/slippage.js +1 -1
  202. package/dist/exchanges/spot-factory.d.ts +3 -3
  203. package/dist/exchanges/spot-factory.js +7 -7
  204. package/dist/exchanges/spot-interface.d.ts +5 -5
  205. package/dist/exchanges/spot-interface.js +2 -2
  206. package/dist/exchanges/spot-result.d.ts +3 -3
  207. package/dist/exchanges/spot-result.js +3 -3
  208. package/dist/exchanges/trading-v2.d.ts +3 -3
  209. package/dist/exchanges/trading-v2.js +9 -6
  210. package/dist/exchanges/txflow/adapter.d.ts +54 -0
  211. package/dist/exchanges/txflow/adapter.js +118 -0
  212. package/dist/exchanges/txflow/client.d.ts +47 -0
  213. package/dist/exchanges/txflow/client.js +153 -0
  214. package/dist/exchanges/txflow/public.d.ts +76 -0
  215. package/dist/exchanges/txflow/public.js +284 -0
  216. package/dist/exchanges/unknown-symbol.d.ts +29 -0
  217. package/dist/exchanges/unknown-symbol.js +51 -0
  218. package/dist/exchanges/variational/adapter.d.ts +71 -0
  219. package/dist/exchanges/variational/adapter.js +137 -0
  220. package/dist/exchanges/variational/client.d.ts +21 -0
  221. package/dist/exchanges/variational/client.js +66 -0
  222. package/dist/exchanges/variational/public.d.ts +126 -0
  223. package/dist/exchanges/variational/public.js +430 -0
  224. package/dist/exchanges/venue-types.d.ts +6 -7
  225. package/dist/exchanges/venue-types.js +2 -2
  226. package/dist/exchanges/venue.d.ts +51 -8
  227. package/dist/exchanges/venue.js +54 -4
  228. package/dist/exchanges/venues/arcus.d.ts +1 -2
  229. package/dist/exchanges/venues/arcus.js +5 -4
  230. package/dist/exchanges/venues/aster.d.ts +2 -2
  231. package/dist/exchanges/venues/aster.js +6 -4
  232. package/dist/exchanges/venues/create-helpers.d.ts +1 -1
  233. package/dist/exchanges/venues/create-helpers.js +3 -3
  234. package/dist/exchanges/venues/extended.d.ts +21 -0
  235. package/dist/exchanges/venues/extended.js +96 -0
  236. package/dist/exchanges/venues/grvt.d.ts +30 -0
  237. package/dist/exchanges/venues/grvt.js +98 -0
  238. package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
  239. package/dist/exchanges/venues/hyperliquid.js +10 -8
  240. package/dist/exchanges/venues/index.d.ts +7 -2
  241. package/dist/exchanges/venues/index.js +8 -2
  242. package/dist/exchanges/venues/lighter-family.d.ts +7 -9
  243. package/dist/exchanges/venues/lighter-family.js +9 -11
  244. package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
  245. package/dist/exchanges/venues/lighter-rh.js +7 -5
  246. package/dist/exchanges/venues/lighter.d.ts +1 -1
  247. package/dist/exchanges/venues/lighter.js +5 -3
  248. package/dist/exchanges/venues/ondo.d.ts +1 -1
  249. package/dist/exchanges/venues/ondo.js +5 -3
  250. package/dist/exchanges/venues/pacifica.d.ts +2 -2
  251. package/dist/exchanges/venues/pacifica.js +6 -4
  252. package/dist/exchanges/venues/qfex.d.ts +1 -1
  253. package/dist/exchanges/venues/qfex.js +4 -2
  254. package/dist/exchanges/venues/risex-release.d.ts +14 -0
  255. package/dist/exchanges/venues/risex-release.js +14 -0
  256. package/dist/exchanges/venues/risex.d.ts +31 -0
  257. package/dist/exchanges/venues/risex.js +116 -0
  258. package/dist/exchanges/venues/txflow.d.ts +18 -0
  259. package/dist/exchanges/venues/txflow.js +77 -0
  260. package/dist/exchanges/venues/variational.d.ts +21 -0
  261. package/dist/exchanges/venues/variational.js +85 -0
  262. package/dist/execution-log.d.ts +1 -1
  263. package/dist/funding/legacy-public.d.ts +1 -1
  264. package/dist/funding/legacy-public.js +9 -4
  265. package/dist/funding/normalize.d.ts +1 -1
  266. package/dist/funding/normalize.js +1 -1
  267. package/dist/funding/rates.d.ts +44 -4
  268. package/dist/funding/rates.js +56 -12
  269. package/dist/index.js +7 -6
  270. package/dist/jobs.d.ts +1 -0
  271. package/dist/jobs.js +25 -1
  272. package/dist/landing.d.ts +2 -0
  273. package/dist/landing.js +1 -1
  274. package/dist/mcp-pacifica-earn.d.ts +9 -0
  275. package/dist/mcp-pacifica-earn.js +58 -0
  276. package/dist/mcp-server.js +113 -59
  277. package/dist/pacifica/client.d.ts +67 -4
  278. package/dist/pacifica/client.js +170 -8
  279. package/dist/pacifica/rate-limit.d.ts +144 -0
  280. package/dist/pacifica/rate-limit.js +267 -0
  281. package/dist/pacifica/types/order.d.ts +2 -1
  282. package/dist/plan-executor.js +7 -2
  283. package/dist/rebalance.js +1 -1
  284. package/dist/risk.d.ts +2 -1
  285. package/dist/risk.js +2 -4
  286. package/dist/settings.d.ts +32 -3
  287. package/dist/signer/ows-evm.d.ts +1 -1
  288. package/dist/signer/ows-evm.js +1 -1
  289. package/dist/smart-order.d.ts +1 -1
  290. package/dist/smart-order.js +1 -1
  291. package/dist/strategies/funding-arb.d.ts +1 -1
  292. package/dist/strategies/funding-arb.js +1 -1
  293. package/dist/trade-validator.d.ts +1 -1
  294. package/dist/trade-validator.js +9 -4
  295. package/dist/utils.d.ts +12 -3
  296. package/dist/utils.js +11 -3
  297. package/dist/venue-health.d.ts +40 -0
  298. package/dist/venue-health.js +75 -0
  299. package/dist/withdraw-gate.d.ts +1 -1
  300. package/dist/withdraw-gate.js +1 -1
  301. package/dist/ws/index.d.ts +959 -0
  302. package/dist/ws/index.js +4295 -0
  303. package/dist/ws/node.d.ts +975 -0
  304. package/dist/ws/node.js +4366 -0
  305. package/dist/ws/pacifica-instrument.d.ts +8 -0
  306. package/dist/ws/pacifica-instrument.js +8 -0
  307. package/dist/ws-cli/credentials.d.ts +62 -0
  308. package/dist/ws-cli/credentials.js +126 -0
  309. package/package.json +58 -10
  310. package/skills/perp-cli/SKILL.md +62 -9
  311. package/skills/perp-cli/references/agent-operations.md +7 -0
  312. package/skills/perp-cli/references/commands.md +76 -5
  313. package/skills/perp-cli/references/ws-library.md +492 -0
@@ -12,7 +12,7 @@ import { registerStrategy } from "../strategy-registry.js";
12
12
  import { getFundingHours } from "../../funding.js";
13
13
  import { annualizeHourlyRate } from "../../funding/normalize.js";
14
14
  import { pruneExecutionLog } from "../../execution-log.js";
15
- import { buildAdapterMap, getPerpSymbol, findRate, matchSymbol, fetchRates, scoreFunding, botSpotPerpMatches, assertFundingArbLegs, cancelBotSymbols, collectFundingSince } from "./funding-arb-utils.js";
15
+ import { buildAdapterMap, getPerpSymbol, findRate, cappedLeverage, matchSymbol, fetchRates, scoreFunding, botSpotPerpMatches, assertFundingArbLegs, cancelBotSymbols, collectFundingSince } from "./funding-arb-utils.js";
16
16
  import { SpotPerpExecutor } from "./spot-perp-executor.js";
17
17
  import { findSpotQuoteBalance, getBotSpotAdapter, nonQuoteSpotBalances, spotNetworkOf, spotSharesPerpMargin, spotTokenBase } from "../../exchanges/spot-factory.js";
18
18
  import { getVenue } from "../../exchanges/venue.js";
@@ -285,7 +285,7 @@ export class SpotPerpArbStrategy {
285
285
  ctx.state.set("spaPositions", remaining);
286
286
  ctx.state.set("arbPositions", remaining.length);
287
287
  }
288
- // ── Rotation check (only to perp venues that match the spot leg, §12.4 #6) ──
288
+ // ── Rotation check (only to perp venues that match the spot leg) ──
289
289
  const perpMarketsCache = new Map();
290
290
  const allowedBySpotVenue = new Map();
291
291
  const currentPositions = ctx.state.get("spaPositions");
@@ -313,7 +313,7 @@ export class SpotPerpArbStrategy {
313
313
  const allowed = allowedByBase.get(pos.symbol.toUpperCase());
314
314
  if (allowed === undefined)
315
315
  continue;
316
- const candidates = await this.findPerpCandidates(pos.symbol, adapters, ratesByExchange, allowed);
316
+ const candidates = await this.findPerpCandidates(pos.symbol, adapters, ratesByExchange, allowed, ctx.log);
317
317
  const best = candidates[0];
318
318
  if (!best || best.exchange === pos.perpExchange)
319
319
  continue;
@@ -331,12 +331,12 @@ export class SpotPerpArbStrategy {
331
331
  continue;
332
332
  const perpSym = getPerpSymbol(pos.symbol, pos.perpExchange);
333
333
  const newMaxLev = findRate(ratesByExchange, best.exchange, getPerpSymbol(pos.symbol, best.exchange))?.maxLeverage
334
- ?? findRate(ratesByExchange, best.exchange, pos.symbol)?.maxLeverage ?? 1;
334
+ ?? findRate(ratesByExchange, best.exchange, pos.symbol)?.maxLeverage;
335
335
  const ok = await this.executor.rotateShort({
336
336
  symbol: pos.symbol, perpSymbol: perpSym,
337
337
  currentExchange: pos.perpExchange, newExchange: best.exchange,
338
338
  currentAdapter: oldAdapter, newAdapter,
339
- size: pos.size, leverage: Math.min(p.leverage, newMaxLev), log: ctx.log,
339
+ size: pos.size, leverage: cappedLeverage(p.leverage, [{ exchange: best.exchange, maxLeverage: newMaxLev }], ctx.log), log: ctx.log,
340
340
  });
341
341
  if (ok.ok) {
342
342
  // The spot leg is unchanged: the state entry follows the short to its new venue.
@@ -375,7 +375,7 @@ export class SpotPerpArbStrategy {
375
375
  const spotAdapter = await getBotSpotAdapter(spotExName, spotExAdapter);
376
376
  if (!spotAdapter)
377
377
  continue;
378
- // Canonical symbol + price guard (§12.4 #6): base → perp venues it may hedge on.
378
+ // Canonical symbol + price guard: base → perp venues it may hedge on.
379
379
  const allowedByBase = await this.spotPerpVenues(spotExName, await spotAdapter.getSpotMarkets(), adapters, perpMarketsCache, ctx);
380
380
  for (const [base, allowed] of allowedByBase) {
381
381
  // HL spot: skip tokens not in whitelist
@@ -387,7 +387,7 @@ export class SpotPerpArbStrategy {
387
387
  continue;
388
388
  if (this.quarantined(base, ctx))
389
389
  continue;
390
- const candidates = await this.findPerpCandidates(base, adapters, ratesByExchange, allowed);
390
+ const candidates = await this.findPerpCandidates(base, adapters, ratesByExchange, allowed, ctx.log);
391
391
  for (const cand of candidates) {
392
392
  if (cand.score.annualized < p.min_rate || cand.score.consistency < p.min_consistency)
393
393
  continue;
@@ -501,7 +501,7 @@ export class SpotPerpArbStrategy {
501
501
  return byBase;
502
502
  }
503
503
  /** Find all perp exchanges for a symbol, sorted by annualized rate (highest first). */
504
- async findPerpCandidates(symbol, adapters, ratesByExchange, allowedPerpVenues) {
504
+ async findPerpCandidates(symbol, adapters, ratesByExchange, allowedPerpVenues, log) {
505
505
  const candidates = [];
506
506
  for (const [exName, adapter] of adapters) {
507
507
  if (!allowedPerpVenues.has(exName))
@@ -510,8 +510,20 @@ export class SpotPerpArbStrategy {
510
510
  const ri = findRate(ratesByExchange, exName, perpSym) ?? findRate(ratesByExchange, exName, symbol);
511
511
  if (!ri || ri.rate <= 0)
512
512
  continue;
513
- const score = await this.scoreFundingLocal(adapter, perpSym, exName)
514
- ?? await this.scoreFundingLocal(adapter, symbol, exName);
513
+ // A failed history read is not "too little history": say so, then try the next symbol form;
514
+ // a venue with no readable form is left out this cycle.
515
+ let score = null;
516
+ for (const sym of perpSym === symbol ? [perpSym] : [perpSym, symbol]) {
517
+ try {
518
+ score = await this.scoreFundingLocal(adapter, sym, exName);
519
+ }
520
+ catch (e) {
521
+ log(` [SPA] ${sym} on ${exName}: funding history unreadable — not a candidate this cycle: ${e instanceof Error ? e.message : String(e)}`);
522
+ continue;
523
+ }
524
+ if (score)
525
+ break;
526
+ }
515
527
  if (!score)
516
528
  continue;
517
529
  candidates.push({ exchange: exName, rate: ri.rate, score });
@@ -569,8 +581,7 @@ export class SpotPerpArbStrategy {
569
581
  const perpSymbol = getPerpSymbol(symbol, perpExchange);
570
582
  const rateInfo = findRate(ratesByExchange, perpExchange, perpSymbol)
571
583
  ?? findRate(ratesByExchange, perpExchange, symbol);
572
- const perpMaxLev = rateInfo?.maxLeverage ?? 1;
573
- const leverage = Math.min(p.leverage, perpMaxLev);
584
+ const leverage = cappedLeverage(p.leverage, [{ exchange: perpExchange, maxLeverage: rateInfo?.maxLeverage }], ctx.log);
574
585
  // Pre-entry: verify fundingHours (aster lazy bootstrap may not have run yet)
575
586
  if (perpExchange === "aster" && rateInfo && "getFundingHours" in perpAdapter) {
576
587
  const actualFh = await perpAdapter.getFundingHours(symbol);
@@ -26,7 +26,7 @@ export type ExecutionResult = {
26
26
  /**
27
27
  * closed = position fully closed; remainingSize = partially closed, this size is still open (hedged).
28
28
  * manual = the outcome is unknown / unverified or a leg is unhedged: the caller must quarantine
29
- * the position and never retry it automatically (design §12.3).
29
+ * the position and never retry it automatically.
30
30
  */
31
31
  export type CloseResult = {
32
32
  closed: true;
@@ -140,7 +140,7 @@ export class SpotPerpExecutor {
140
140
  await this.returnToPerp(spotAdapter, actualTransferred, spotExchange, log);
141
141
  return { success: false, error: `pre-trade balance: ${msg}` };
142
142
  }
143
- // Step 1: Buy spot — the hedge is sized to what actually filled (§12.4 #5)
143
+ // Step 1: Buy spot — the hedge is sized to what actually filled
144
144
  let fill;
145
145
  try {
146
146
  fill = await spotMarketFill(spotAdapter, symbol, "buy", matched.size, spotStep);
@@ -10,7 +10,8 @@ export interface EnrichedSnapshot extends MarketSnapshot {
10
10
  bids: [string, string][];
11
11
  asks: [string, string][];
12
12
  };
13
- openInterest: string;
13
+ /** Base-asset open interest; null = not read / not published (never "0" as a stand-in). */
14
+ openInterest: string | null;
14
15
  }
15
16
  export type StrategyAction = {
16
17
  type: "place_order";
@@ -28,7 +28,7 @@ export interface BotTuiState {
28
28
  exchange: string;
29
29
  price: number;
30
30
  volume24h: number;
31
- openInterest: string;
31
+ openInterest: string | null;
32
32
  fundingRate: number;
33
33
  volatility24h: number;
34
34
  positions: Position[];
@@ -78,7 +78,7 @@ export function BotDashboard({ initialState, onQuit, onPause, subscribe }) {
78
78
  const askOrders = state.openOrders.filter((o) => o.side.toLowerCase() === "sell");
79
79
  const hasMultiExchange = (state.exchangeBalances?.length ?? 0) > 1;
80
80
  const isMultiSymbol = state.symbol.toUpperCase() === "ALL";
81
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.strategy, " | ", state.symbol, " | ", state.exchange] }), _jsxs(Text, { color: "gray", children: ["Runtime: ", formatDuration(state.runtime), " | Tick: ", state.tick] })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: phaseColor(state.phase), children: phaseIndicator(state.phase) }) })] }), isMultiSymbol ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Exchanges: ", _jsx(Text, { bold: true, color: "white", children: state.exchangeBalances?.length ?? 1 })] }), _jsxs(Text, { color: "gray", children: ["Positions: ", _jsx(Text, { bold: true, color: state.positions.length > 0 ? "yellow" : "white", children: state.positions.length })] }), _jsxs(Text, { color: "gray", children: ["Total Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(totalEquity) })] }), _jsxs(Text, { color: "gray", children: ["PnL: ", _jsxs(Text, { bold: true, color: state.totalPnl >= 0 ? "green" : "red", children: ["$", state.totalPnl.toFixed(2)] })] })] })) : (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Price: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.price.toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["Vol24h: ", _jsx(Text, { color: "white", children: Number.isFinite(state.volatility24h) ? `${state.volatility24h.toFixed(1)}%` : "unknown" })] }), _jsxs(Text, { color: "gray", children: ["FR: ", _jsxs(Text, { color: state.fundingRate >= 0 ? "green" : "red", children: [(state.fundingRate * 100).toFixed(4), "%"] })] }), _jsxs(Text, { color: "gray", children: ["OI: ", _jsx(Text, { color: "white", children: state.openInterest })] })] })), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Account" }), _jsxs(Text, { children: ["Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(state.equity) }), _jsxs(Text, { color: "gray", children: [" (peak: ", usd(state.peakEquity), ")"] })] }), _jsxs(Text, { children: ["Daily P&L: ", _jsxs(Text, { bold: true, color: pnlColor(state.dailyPnl), children: ["$", state.dailyPnl.toFixed(2)] })] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: ["Drawdown: ", _jsx(Text, { bold: true, color: drawdown === null ? "gray" : drawdown > 0 ? "red" : "green", children: drawdown === null ? "unknown" : `${usd(drawdown)} (${drawdownPct.toFixed(1)}%)` })] }), _jsxs(Text, { children: ["Orders: ", _jsx(Text, { bold: true, color: "white", children: state.ordersPlaced }), _jsx(Text, { color: "gray", children: " | Fills: " }), _jsx(Text, { bold: true, color: "white", children: state.fills }), _jsx(Text, { color: "gray", children: " | Total P&L: " }), _jsxs(Text, { bold: true, color: pnlColor(state.totalPnl), children: ["$", state.totalPnl.toFixed(2)] })] })] })] }), state.exchangeBalances && state.exchangeBalances.length > 1 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Exchange Balances" }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: pad("Exchange", 16) }), _jsx(Text, { color: "gray", children: pad("Equity", 14) }), _jsx(Text, { color: "gray", children: "Available" })] }), state.exchangeBalances.map((eb, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "white", children: pad(eb.exchange, 16) }), eb.equity === null ? (_jsxs(Text, { color: "red", children: ["unavailable", eb.error ? ` (${eb.error})` : ""] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "white", children: pad(`$${parseFloat(eb.equity).toFixed(2)}`, 14) }), _jsx(Text, { color: "gray", children: eb.available === null ? "unavailable" : `$${parseFloat(eb.available).toFixed(2)}` })] }))] }, i)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Positions" }), positionErrors.map((pe, i) => (_jsxs(Text, { color: "red", children: [" ", pe.exchange, ": unavailable (", pe.error, ")"] }, `err-${i}`))), state.positions.length === 0 ? (positionErrors.length === 0 && _jsx(Text, { color: "gray", children: " No open positions" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "gray", children: pad("Exchange", 14) }), _jsx(Text, { color: "gray", children: pad("Symbol", 10) }), _jsx(Text, { color: "gray", children: pad("Side", 6) }), _jsx(Text, { color: "gray", children: pad("Size", 12) }), _jsx(Text, { color: "gray", children: pad("Entry", 12) }), _jsx(Text, { color: "gray", children: pad("Mark", 12) }), _jsx(Text, { color: "gray", children: "uPnL" })] }), state.positions.map((p, i) => {
81
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.strategy, " | ", state.symbol, " | ", state.exchange] }), _jsxs(Text, { color: "gray", children: ["Runtime: ", formatDuration(state.runtime), " | Tick: ", state.tick] })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: phaseColor(state.phase), children: phaseIndicator(state.phase) }) })] }), isMultiSymbol ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Exchanges: ", _jsx(Text, { bold: true, color: "white", children: state.exchangeBalances?.length ?? 1 })] }), _jsxs(Text, { color: "gray", children: ["Positions: ", _jsx(Text, { bold: true, color: state.positions.length > 0 ? "yellow" : "white", children: state.positions.length })] }), _jsxs(Text, { color: "gray", children: ["Total Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(totalEquity) })] }), _jsxs(Text, { color: "gray", children: ["PnL: ", _jsxs(Text, { bold: true, color: state.totalPnl >= 0 ? "green" : "red", children: ["$", state.totalPnl.toFixed(2)] })] })] })) : (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Price: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.price.toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["Vol24h: ", _jsx(Text, { color: "white", children: Number.isFinite(state.volatility24h) ? `${state.volatility24h.toFixed(1)}%` : "unknown" })] }), _jsxs(Text, { color: "gray", children: ["FR: ", _jsxs(Text, { color: state.fundingRate >= 0 ? "green" : "red", children: [(state.fundingRate * 100).toFixed(4), "%"] })] }), _jsxs(Text, { color: "gray", children: ["OI: ", _jsx(Text, { color: "white", children: state.openInterest ?? "-" })] })] })), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Account" }), _jsxs(Text, { children: ["Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(state.equity) }), _jsxs(Text, { color: "gray", children: [" (peak: ", usd(state.peakEquity), ")"] })] }), _jsxs(Text, { children: ["Daily P&L: ", _jsxs(Text, { bold: true, color: pnlColor(state.dailyPnl), children: ["$", state.dailyPnl.toFixed(2)] })] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: ["Drawdown: ", _jsx(Text, { bold: true, color: drawdown === null ? "gray" : drawdown > 0 ? "red" : "green", children: drawdown === null ? "unknown" : `${usd(drawdown)} (${drawdownPct.toFixed(1)}%)` })] }), _jsxs(Text, { children: ["Orders: ", _jsx(Text, { bold: true, color: "white", children: state.ordersPlaced }), _jsx(Text, { color: "gray", children: " | Fills: " }), _jsx(Text, { bold: true, color: "white", children: state.fills }), _jsx(Text, { color: "gray", children: " | Total P&L: " }), _jsxs(Text, { bold: true, color: pnlColor(state.totalPnl), children: ["$", state.totalPnl.toFixed(2)] })] })] })] }), state.exchangeBalances && state.exchangeBalances.length > 1 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Exchange Balances" }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: pad("Exchange", 16) }), _jsx(Text, { color: "gray", children: pad("Equity", 14) }), _jsx(Text, { color: "gray", children: "Available" })] }), state.exchangeBalances.map((eb, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "white", children: pad(eb.exchange, 16) }), eb.equity === null ? (_jsxs(Text, { color: "red", children: ["unavailable", eb.error ? ` (${eb.error})` : ""] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "white", children: pad(`$${parseFloat(eb.equity).toFixed(2)}`, 14) }), _jsx(Text, { color: "gray", children: eb.available === null ? "unavailable" : `$${parseFloat(eb.available).toFixed(2)}` })] }))] }, i)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Positions" }), positionErrors.map((pe, i) => (_jsxs(Text, { color: "red", children: [" ", pe.exchange, ": unavailable (", pe.error, ")"] }, `err-${i}`))), state.positions.length === 0 ? (positionErrors.length === 0 && _jsx(Text, { color: "gray", children: " No open positions" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "gray", children: pad("Exchange", 14) }), _jsx(Text, { color: "gray", children: pad("Symbol", 10) }), _jsx(Text, { color: "gray", children: pad("Side", 6) }), _jsx(Text, { color: "gray", children: pad("Size", 12) }), _jsx(Text, { color: "gray", children: pad("Entry", 12) }), _jsx(Text, { color: "gray", children: pad("Mark", 12) }), _jsx(Text, { color: "gray", children: "uPnL" })] }), state.positions.map((p, i) => {
82
82
  const upnl = parseFloat(p.unrealizedPnl);
83
83
  return (_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "cyan", children: pad(p.exchange ?? "—", 14) }), _jsx(Text, { color: "white", children: pad(p.symbol, 10) }), _jsx(Text, { color: p.side.toLowerCase() === "long" ? "green" : "red", children: pad(p.side, 6) }), _jsx(Text, { color: "white", children: pad(p.size, 12) }), _jsx(Text, { color: "white", children: pad(p.entryPrice, 12) }), _jsx(Text, { color: "white", children: pad(p.markPrice, 12) }), _jsx(Text, { color: isNaN(upnl) ? "gray" : pnlColor(upnl), children: isNaN(upnl) ? "$—" : `$${upnl.toFixed(2)}` })] }, i));
84
84
  })] }))] }), openOrdersError !== null ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["Open orders unavailable (", openOrdersError, ")"] }) })) : (_jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "green", bold: true, children: ["Bids (", bidOrders.length, ")"] }), bidOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (bidOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "green", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] }), _jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "red", bold: true, children: ["Asks (", askOrders.length, ")"] }), askOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (askOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "red", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] })] })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: state.strategyState.fundingTotal !== undefined ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "cyan", bold: true, children: "Funding & Arb" }), _jsxs(Box, { flexDirection: "row", marginLeft: 2, children: [_jsx(Text, { color: "gray", children: "Funding Income: " }), _jsx(Text, { color: "white", children: String(state.strategyState.fundingTotal) }), _jsx(Text, { color: "gray", children: " Positions: " }), _jsx(Text, { color: "white", children: String(state.strategyState.arbPositions ?? 0) })] }), Array.isArray(state.strategyState.arbPositionDetails) &&
package/dist/cache.js CHANGED
@@ -7,6 +7,7 @@
7
7
  import { mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, unlinkSync } from "fs";
8
8
  import { join } from "path";
9
9
  import { tmpdir } from "os";
10
+ import { randomBytes } from "crypto";
10
11
  const CACHE_DIR = join(tmpdir(), "perp-cli-cache");
11
12
  // TTL presets
12
13
  export const TTL_ACCOUNT = 15_000; // 15s — balance, positions, orders (ticks are 30s; 15s ensures same-tick dedup)
@@ -41,16 +42,21 @@ export function getCached(key) {
41
42
  }
42
43
  export function setCached(key, data, ttlMs) {
43
44
  ensureDir();
45
+ const file = join(CACHE_DIR, `${safeKey(key)}.json`);
46
+ // Unique per writer: concurrent processes writing the same key must not share one tmp file
47
+ // (a rename of another writer's half-written tmp would publish a partial entry).
48
+ const tmp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
44
49
  try {
45
50
  const entry = { ts: Date.now(), ttl: ttlMs, data };
46
- const content = JSON.stringify(entry);
47
- const file = join(CACHE_DIR, `${safeKey(key)}.json`);
48
- const tmp = file + ".tmp";
49
- writeFileSync(tmp, content);
51
+ writeFileSync(tmp, JSON.stringify(entry));
50
52
  renameSync(tmp, file);
51
53
  }
52
54
  catch {
53
- // cache write failure is non-fatal
55
+ // cache write failure is non-fatal; never leave this writer's tmp file behind
56
+ try {
57
+ unlinkSync(tmp);
58
+ }
59
+ catch { /* not written */ }
54
60
  }
55
61
  }
56
62
  /**
@@ -94,7 +100,7 @@ export function invalidateCache(keyPrefix) {
94
100
  const files = readdirSync(CACHE_DIR);
95
101
  for (const f of files) {
96
102
  if (!f.endsWith(".json"))
97
- continue;
103
+ continue; // also skips in-flight "<key>.json.<pid>.<rand>.tmp" files
98
104
  if (!prefix || f.startsWith(prefix)) {
99
105
  try {
100
106
  unlinkSync(join(CACHE_DIR, f));
@@ -28,8 +28,23 @@ export declare function landingData(program: Command, version: string): {
28
28
  name: string;
29
29
  description: string;
30
30
  }[];
31
+ venues: {
32
+ id: string;
33
+ displayName: string;
34
+ roles: string[];
35
+ apiStatus: "official" | "unofficial";
36
+ note: string | null;
37
+ }[];
31
38
  };
32
39
  export declare function buildProgram(opts: {
33
40
  defaultExchange: string;
34
41
  version: string;
35
42
  }): CliProgram;
43
+ /**
44
+ * The trading-role gate at command resolution: `perp -e <venue> trade <write>`, every
45
+ * `perp trade multi <venue>:…` leg and `perp wallet agent approve|import|revoke|rotate <venue>`
46
+ * (revoke --force / --local-only excepted: local cleanup only) and every `strategy … --exchanges` leg refuse with roleUnsupported() (CAPABILITY_UNSUPPORTED) when the venue has no trading role. Runs
47
+ * before the dry-run guard and before any action, so a dry-run plan can never present a gated
48
+ * venue as tradable. Unknown venue names are left to the command's own UNKNOWN_VENUE handling.
49
+ */
50
+ export declare function assertCommandVenueRole(actionCommand: Command, exchangeOpt: string): void;
@@ -6,13 +6,15 @@ import { Command } from "commander";
6
6
  import { cliCredentialOptsFrom, hasCredentials, resolveCredentials } from "./config.js";
7
7
  import { PacificaAdapter } from "./exchanges/pacifica.js";
8
8
  import { HyperliquidAdapter } from "./exchanges/hyperliquid.js";
9
- import { getVenue, listVenues, resolveVenueId, READ_ONLY_CREDENTIALS } from "./exchanges/venue.js";
9
+ import { getVenue, listVenues, resolveVenueId, roleUnsupported, tryResolveVenueId, READ_ONLY_CREDENTIALS } from "./exchanges/venue.js";
10
10
  import { registerAdapterNetwork } from "./exchanges/adapter-network.js";
11
11
  import "./exchanges/venues/index.js";
12
12
  import { PerpError } from "./errors.js";
13
13
  import { registerMarketCommands } from "./commands/market.js";
14
14
  import { registerAccountCommands } from "./commands/account.js";
15
15
  import { registerOutcomeCommands } from "./commands/outcome.js";
16
+ import { registerOptionsCommands } from "./commands/options.js";
17
+ import { registerPacificaEarnCommands } from "./commands/pacifica-earn.js";
16
18
  import { registerTradeCommands } from "./commands/trade.js";
17
19
  // manage commands now register under `wallet manage ...` via registerWalletCommands.
18
20
  // stream commands removed — WS feeds still used by dashboard/event-stream internally
@@ -86,6 +88,11 @@ export function landingData(program, version) {
86
88
  return {
87
89
  name: program.name(),
88
90
  version,
91
+ // Registry facts only (no reads): an "unofficial" venue is read through an undocumented API.
92
+ venues: listVenues().map(v => ({
93
+ id: v.id, displayName: v.displayName, roles: [...v.roles], apiStatus: v.apiStatus.kind,
94
+ note: v.apiStatus.kind === "unofficial" ? v.apiStatus.note : null,
95
+ })),
89
96
  usage: `${program.name()} [options] <command> [args] (add --json for one JSON document on stdout)`,
90
97
  help: `${program.name()} <command> --help`,
91
98
  commands: program.commands
@@ -131,9 +138,9 @@ export function buildProgram(opts) {
131
138
  });
132
139
  let _adapter = null;
133
140
  /**
134
- * One adapter per `${venueId}:${network}:${wallet}:${hlDex}` (design §3 L1 cache key).
141
+ * One adapter per `${venueId}:${network}:${wallet}:${hlDex}`.
135
142
  * Every adapter is built by the venue definition's create(ctx) — the single
136
- * creation path shared with the MCP server (design §10.2).
143
+ * creation path shared with the MCP server.
137
144
  */
138
145
  const _adapters = new Map();
139
146
  function cliCredentialOpts() {
@@ -147,7 +154,7 @@ export function buildProgram(opts) {
147
154
  const wallet = opts.ows ? `ows:${opts.ows}` : (opts.wallet ?? "default");
148
155
  return `${venue}:${network}:${wallet}:${hlDex ?? "-"}`;
149
156
  }
150
- /** restrictedRegions notice: stderr, once per venue per process (design §9.1 #2 — warn, never block). */
157
+ /** restrictedRegions notice: stderr, once per venue per process (warn, never block). */
151
158
  const _restrictedWarned = new Set();
152
159
  function warnRestrictedRegions(def) {
153
160
  if (def.restrictedRegions.length === 0 || _restrictedWarned.has(def.id))
@@ -167,6 +174,10 @@ export function buildProgram(opts) {
167
174
  remediation: `${def.id} is market-data only — use the funding/market public commands (e.g. perp arb scan)`,
168
175
  });
169
176
  }
177
+ // A market-data-only venue (TxFlow: unofficial read-only API) never gets a credentialed adapter:
178
+ // every account / trading / funds / bot path refuses here, before any credential or request.
179
+ if (!publicOnly && !def.roles.includes("account"))
180
+ throw roleUnsupported(def, "account");
170
181
  const network = currentNetwork();
171
182
  const hlDex = def.id === "hyperliquid" ? dex : null;
172
183
  const key = publicOnly ? `${def.id}:${network}:public:${hlDex ?? "-"}` : adapterCacheKey(def.id, network, hlDex);
@@ -246,6 +257,8 @@ export function buildProgram(opts) {
246
257
  registerMarketCommands(program, getPublicAdapter, isJson, getPublicAdapterForExchange, getAdapter);
247
258
  registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange);
248
259
  registerOutcomeCommands(program, getAdapterForExchange, isJson);
260
+ registerOptionsCommands(program, isJson);
261
+ registerPacificaEarnCommands(program, getAdapterForExchange, isJson);
249
262
  registerTradeCommands(program, getAdapter, isJson, isDryRunFlag, getAdapterForExchange);
250
263
  // manage tree wired inside registerWalletCommands below.
251
264
  // stream commands removed
@@ -273,7 +286,15 @@ export function buildProgram(opts) {
273
286
  // registerFundsCommands above.
274
287
  // Background processes & strategies
275
288
  registerBackgroundCommands(program, isJson, getAdapterForExchange);
276
- registerStrategyCommands(program, getAdapter, getAdapterForExchange, isJson);
289
+ // Every strategy command (bots, plans, TWAP / DCA / grid, `strategy run`, `--exchanges` legs,
290
+ // config-file venues) builds its adapters through these: a venue without the trading role
291
+ // (TxFlow, release-gated RISEx) is refused here, before any key is opened — also under --dry-run.
292
+ const strategyVenueGate = (venue) => {
293
+ const def = getVenue(venue);
294
+ if (!def.roles.includes("trading"))
295
+ throw roleUnsupported(def, "automation");
296
+ };
297
+ registerStrategyCommands(program, async () => { strategyVenueGate(getExchange()); return getAdapter(); }, async (exchange) => { strategyVenueGate(exchange); return getAdapterForExchange(exchange); }, isJson);
277
298
  // Agent-friendly commands
278
299
  registerRiskCommands(program, getAdapterForExchange, isJson);
279
300
  registerHistoryCommands(program, isJson, getAdapterForExchange);
@@ -286,7 +307,12 @@ export function buildProgram(opts) {
286
307
  registerPortfolioCommand(program, getAdapterForExchange, isJson, version, getHLAdapterForDex);
287
308
  registerInitCommand(program);
288
309
  registerAlertCommands(program, isJson);
310
+ installNestedHelp(program);
311
+ installMissingSubcommandError(program);
289
312
  // Switch shared API URLs if --network testnet is used
313
+ // A venue without the trading role (market-data-only TxFlow, release-gated RISEx) refuses every
314
+ // trading command before anything else — also under --dry-run, whose plans skip the adapter.
315
+ program.hook("preAction", (_root, actionCommand) => assertCommandVenueRole(actionCommand, program.opts().exchange));
290
316
  // Validates -n and enforces default-deny --dry-run before any action runs.
291
317
  installDryRunGuard(program);
292
318
  // Secrets on argv (review L10): -k is rejected outright; --passphrase still works but warns loudly.
@@ -301,3 +327,97 @@ export function buildProgram(opts) {
301
327
  program.hook("preAction", () => setQuarantineNetwork(networkOf(program)));
302
328
  return { program, getAdapterForExchange, isJson };
303
329
  }
330
+ /** `perp trade <leaf>` commands that only read (order status, fills, P&L tracking, spot reads). */
331
+ const TRADE_READ_COMMANDS = new Set(["status", "fills", "pnl-track", "spot-orders", "spot-balance"]);
332
+ /**
333
+ * The trading-role gate at command resolution: `perp -e <venue> trade <write>`, every
334
+ * `perp trade multi <venue>:…` leg and `perp wallet agent approve|import|revoke|rotate <venue>`
335
+ * (revoke --force / --local-only excepted: local cleanup only) and every `strategy … --exchanges` leg refuse with roleUnsupported() (CAPABILITY_UNSUPPORTED) when the venue has no trading role. Runs
336
+ * before the dry-run guard and before any action, so a dry-run plan can never present a gated
337
+ * venue as tradable. Unknown venue names are left to the command's own UNKNOWN_VENUE handling.
338
+ */
339
+ export function assertCommandVenueRole(actionCommand, exchangeOpt) {
340
+ const path = commandPath(actionCommand).split(" ");
341
+ const leaf = path[path.length - 1];
342
+ let venueArgs;
343
+ let feature;
344
+ if (leaf === "multi" && (path[0] === "trade" || path.length === 1)) {
345
+ venueArgs = actionCommand.args.map(leg => leg.split(":")[0]);
346
+ feature = "trading";
347
+ }
348
+ else if (path[0] === "strategy" && typeof actionCommand.opts().exchanges === "string") {
349
+ // `--exchanges` legs (strategy run / funding-arb / quick-arb / delta-neutral, incl. their defaults):
350
+ // refused before the first leg's adapter is built. The -e venue is gated by the strategy adapter factory.
351
+ venueArgs = String(actionCommand.opts().exchanges).split(",").map(v => v.trim()).filter(v => v !== "");
352
+ feature = "automation";
353
+ }
354
+ else if (path[0] === "trade" && path.length === 2 && !TRADE_READ_COMMANDS.has(leaf)) {
355
+ venueArgs = [exchangeOpt];
356
+ feature = "trading";
357
+ }
358
+ else if (path[0] === "wallet" && path[1] === "agent" && ["approve", "import", "revoke", "rotate"].includes(path[2] ?? "")) {
359
+ // `revoke --force` / `--local-only` only clears local records (nothing signed or sent): allowed.
360
+ const o = actionCommand.opts();
361
+ if (path[2] === "revoke" && (o.force === true || o.localOnly === true))
362
+ return;
363
+ venueArgs = actionCommand.args.slice(0, 1);
364
+ feature = path[2] === "import" ? "agentImport" : `agent ${path[2]}`;
365
+ }
366
+ else {
367
+ return;
368
+ }
369
+ for (const v of venueArgs) {
370
+ const id = tryResolveVenueId(v);
371
+ if (id !== null && !getVenue(id).roles.includes("trading"))
372
+ throw roleUnsupported(getVenue(id), feature);
373
+ }
374
+ }
375
+ /** `perp a b c` path of a command (root name excluded). */
376
+ function commandPath(cmd) {
377
+ const names = [];
378
+ for (let c = cmd; c !== null && c.parent !== null; c = c.parent)
379
+ names.unshift(c.name());
380
+ return names.join(" ");
381
+ }
382
+ /**
383
+ * `perp help <group> <leaf> ...` shows the help of the deepest named command. Commander's built-in
384
+ * root help command resolves only its first operand, so `perp help market list` printed the
385
+ * `market` group help. Groups keep their own built-in `help` subcommand (`perp market help list`).
386
+ */
387
+ function installNestedHelp(program) {
388
+ program.helpCommand(false);
389
+ program
390
+ .command("help [command...]")
391
+ .description("display help for command (any depth: perp help trade limit)")
392
+ .action((names) => {
393
+ let cmd = program;
394
+ for (const name of names) {
395
+ const sub = cmd.commands.find(c => c.name() === name || c.aliases().includes(name));
396
+ if (sub === undefined) {
397
+ const where = cmd === program ? "perp" : `perp ${commandPath(cmd)}`;
398
+ throw new PerpError("INVALID_PARAMS", `Unknown command '${name}' under '${where}'`, {
399
+ remediation: `Available: ${cmd.commands.map(c => c.name()).join(", ")} — see: ${where} --help`,
400
+ });
401
+ }
402
+ cmd = sub;
403
+ }
404
+ cmd.help();
405
+ });
406
+ }
407
+ /**
408
+ * A command group run without a subcommand (`perp history`): Commander prints the group's help as
409
+ * an error. In --json mode that reached stdout as a CLI_ERROR whose message was the usage text;
410
+ * it is now one INVALID_PARAMS document naming the subcommands, with remediation (exit 1).
411
+ * Human mode keeps the usage listing on stderr.
412
+ */
413
+ function installMissingSubcommandError(program) {
414
+ program.on("beforeAllHelp", (ctx) => {
415
+ if (!ctx.error || program.opts().json !== true)
416
+ return;
417
+ const path = `perp ${commandPath(ctx.command)}`.trim();
418
+ const subs = ctx.command.commands.map(c => c.name()).filter(n => n !== "help");
419
+ throw new PerpError("INVALID_PARAMS", `'${path}' needs a subcommand: ${subs.join(", ")}`, {
420
+ remediation: `Run one of them, e.g. ${path} ${subs[0]} — list them with: ${path} --help`,
421
+ });
422
+ });
423
+ }
@@ -90,6 +90,13 @@ export declare function importSecretLineCount(venue: VenueId): number;
90
90
  export declare function importSecretFromLines(venue: VenueId, lines: string[]): ImportSecret;
91
91
  /** Read the secret: hidden TTY prompt, else piped stdin (ondo: 2 lines key id + secret; others: 1 line). */
92
92
  export declare function readImportSecret(venue: VenueId): Promise<ImportSecret>;
93
+ /**
94
+ * Venues whose keys cannot be imported. TxFlow: market-data-only (unofficial read-only API).
95
+ * RISEx: perp-cli trades only with a signer it registered itself and then restricted ON-CHAIN to
96
+ * the trading permission; a signer created in the RISEx app keeps every permission (including
97
+ * deposit / withdraw), so it is never stored here.
98
+ */
99
+ export declare function assertImportSupported(venue: VenueId, network: Network): void;
93
100
  /** Venues whose import cannot be checked against the venue (stored as "unverified"). */
94
101
  export declare const UNVERIFIED_IMPORT_VENUES: readonly VenueId[];
95
102
  /**
@@ -24,7 +24,8 @@
24
24
  import chalk from "chalk";
25
25
  import { PerpError, classifyError } from "../errors.js";
26
26
  import { acquireLock, releaseLock, getAgent, setAgent, deleteAgent, listAgents, agentNetworkOf, agentSourceOf, getVenueAgent, setVenueAgent, deleteVenueAgent, } from "../agent-wallet/store.js";
27
- import { getVenue, networkConfig } from "../exchanges/venue.js";
27
+ import { getVenue, networkConfig, roleUnsupported } from "../exchanges/venue.js";
28
+ import { unsupported } from "../exchanges/trading-v2.js";
28
29
  import { LIGHTER_RH_RESERVED_KEY_INDEXES } from "../exchanges/venues/lighter-family.js";
29
30
  import { loadOws } from "../signer/ows-loader.js";
30
31
  import { defaultAgentName, isLighterFamilyVenue, parseExpiresIn, promptHidden, qfexWithdrawKeyWarning } from "./agent.js";
@@ -67,6 +68,11 @@ export function importSecretLabel(venue) {
67
68
  case "arcus": return "Arcus API key private key (Ed25519)";
68
69
  case "ondo": return "Ondo API secret";
69
70
  case "qfex": return "QFEX API secret";
71
+ case "risex": return "RISEx signer private key";
72
+ case "txflow": return "TxFlow key (not supported)";
73
+ case "variational": return "Variational key (not supported)";
74
+ case "grvt": return "GRVT key (not supported)";
75
+ case "extended": return "Extended key (not supported)";
70
76
  }
71
77
  }
72
78
  /**
@@ -154,6 +160,20 @@ function redacted(err, secrets, venue) {
154
160
  details.remediation = scrub(e.remediation, forms);
155
161
  return new PerpError(e.code, scrub(e.message, forms), details);
156
162
  }
163
+ /**
164
+ * Venues whose keys cannot be imported. TxFlow: market-data-only (unofficial read-only API).
165
+ * RISEx: perp-cli trades only with a signer it registered itself and then restricted ON-CHAIN to
166
+ * the trading permission; a signer created in the RISEx app keeps every permission (including
167
+ * deposit / withdraw), so it is never stored here.
168
+ */
169
+ export function assertImportSupported(venue, network) {
170
+ const def = getVenue(venue);
171
+ if (!def.roles.includes("trading"))
172
+ throw roleUnsupported(def, "agentImport");
173
+ if (venue === "risex") {
174
+ throw unsupported("risex", "agentImport", "perp-cli only uses RISEx signers it registered and restricted on-chain to trading-only permissions; an app-created signer keeps deposit / withdraw permissions", `Register a restricted signer with your master wallet: perp${network === "testnet" ? " -n testnet" : ""} wallet agent approve risex --master <ows-wallet>`);
175
+ }
176
+ }
157
177
  /** Venues whose import cannot be checked against the venue (stored as "unverified"). */
158
178
  export const UNVERIFIED_IMPORT_VENUES = ["pacifica"];
159
179
  /**
@@ -722,6 +742,8 @@ async function importQfex(p, agentName, s, replacing) {
722
742
  * appears in a thrown error.
723
743
  */
724
744
  export async function importVenueAgent(p) {
745
+ // Refused before the secret is even inspected (nothing read, stored or sent).
746
+ assertImportSupported(p.venue, p.network);
725
747
  const secrets = secretsOf(p.secret);
726
748
  if (secrets.some(x => x.trim().length === 0))
727
749
  throw new PerpError("INVALID_PARAMS", "Empty key — nothing was imported");
@@ -751,6 +773,13 @@ export async function importVenueAgent(p) {
751
773
  case "lighter":
752
774
  case "lighter-rh":
753
775
  return await importLighter(p, p.venue, agentName, key, replacing);
776
+ case "risex":
777
+ case "txflow":
778
+ case "variational":
779
+ case "grvt":
780
+ case "extended":
781
+ // Unreachable: assertImportSupported() refused these before the lock.
782
+ throw new PerpError("CAPABILITY_UNSUPPORTED", `${p.venue} keys cannot be imported`);
754
783
  }
755
784
  }
756
785
  catch (err) {
@@ -1,5 +1,5 @@
1
1
  import { Command, Option } from "commander";
2
- import { loadSettings, type OndoAgentMeta, type ArcusAgentMeta, type QfexAgentMeta } from "../settings.js";
2
+ import { loadSettings, type OndoAgentMeta, type ArcusAgentMeta, type QfexAgentMeta, type RisexAgentMeta } from "../settings.js";
3
3
  import "../exchanges/venues/index.js";
4
4
  import type { Network, VenueId } from "../exchanges/venue-types.js";
5
5
  import { type LighterVenueId } from "../exchanges/venues/lighter-family.js";
@@ -27,7 +27,7 @@ export interface VerifyResult {
27
27
  /** Extract the agent list from a Pacifica /agent/list envelope; throws on an unrecognized shape. */
28
28
  export declare function parsePacAgentList(json: unknown): unknown[];
29
29
  /**
30
- * Confirmation of the account a QFEX key was resolved to (design §14.12 #9). In a terminal: a
30
+ * Confirmation of the account a QFEX key was resolved to. In a terminal: a
31
31
  * y/N prompt showing the UUID. Without one (piped stdin, --json): only an explicit --account-id
32
32
  * that equals the resolved account counts — the resolved UUID is printed on stderr either way.
33
33
  */
@@ -94,6 +94,9 @@ export type ApproveAgentOutcome = {
94
94
  } | {
95
95
  venue: "arcus";
96
96
  meta: ArcusAgentMeta;
97
+ } | {
98
+ venue: "risex";
99
+ meta: RisexAgentMeta;
97
100
  } | {
98
101
  venue: LighterVenueId;
99
102
  agentName: string;
@@ -131,7 +134,7 @@ export declare function qfexWithdrawKeyWarning(meta: Pick<QfexAgentMeta, "agentN
131
134
  /** Venues `wallet agent rotate` (and `approve --rotate`) can rotate. */
132
135
  type RotatableVenue = "aster" | "hyperliquid" | "pacifica" | LighterVenueId;
133
136
  type RotatedAgentOutcome = Exclude<ApproveAgentOutcome, {
134
- venue: "ondo" | "arcus";
137
+ venue: "ondo" | "arcus" | "risex";
135
138
  }>;
136
139
  export interface RotateAgentParams {
137
140
  exchange: RotatableVenue;