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
@@ -1,99 +1,13 @@
1
1
  import { makeTable, formatUsd, formatPercent, formatPnl, printJson, jsonOk, withJsonErrors, logSettledRejections } from "../utils.js";
2
2
  import chalk from "chalk";
3
3
  import { toHourlyRate, annualizeHourlyRate, estimateHourlyFundingFromHourly } from "../funding.js";
4
- import { fetchAllFundingRates, fetchSymbolFundingRates, TOP_SYMBOLS, } from "../funding-rates.js";
4
+ import { fetchAllFundingRates, fetchCanonicalPriceGroups, fetchSymbolFundingRates, TOP_SYMBOLS, } from "../funding-rates.js";
5
5
  import { saveFundingSnapshot, getHistoricalRates, getCompoundedAnnualReturn, getExchangeCompoundingHours, } from "../funding-history.js";
6
6
  import { getVenue, listVenuesWithRole, tryResolveVenueId } from "../exchanges/venue.js";
7
7
  import "../exchanges/venues/index.js";
8
- import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates, fetchPacificaPricesRaw, parsePacificaRaw, fetchHyperliquidAllMidsRaw, fetchLighterOrderBookDetailsRaw, } from "../shared-api.js";
8
+ import { MAX_PRICE_DEVIATION_PCT } from "../exchanges/index.js";
9
+ import { fetchPacificaPricesRaw, parsePacificaRaw, fetchHyperliquidAllMidsRaw, fetchLighterOrderBookDetailsRaw, } from "../shared-api.js";
9
10
  import { scanDexArb } from "../dex-asset-map.js";
10
- async function fetchPacificaRates() {
11
- const assets = await fetchPacificaPrices();
12
- return assets.map(p => ({
13
- exchange: "pacifica",
14
- symbol: p.symbol,
15
- fundingRate: p.funding,
16
- markPrice: p.mark,
17
- }));
18
- }
19
- async function fetchHyperliquidRates() {
20
- const assets = await fetchHyperliquidMeta();
21
- return assets.map(a => ({
22
- exchange: "hyperliquid",
23
- symbol: a.symbol,
24
- fundingRate: a.funding,
25
- markPrice: a.markPx,
26
- }));
27
- }
28
- async function fetchAsterRates() {
29
- // SSOT rule #2: do NOT silently coerce missing markPrice / lastFundingRate
30
- // to 0 — that would corrupt comparisons against other DEXs. Skip symbols
31
- // whose Aster row is malformed instead. An endpoint failure PROPAGATES: the
32
- // caller (handleBasisScan) runs every DEX under Promise.allSettled and reports
33
- // the rejection on stderr — Aster is never silently absent from the scan.
34
- const res = await fetch("https://fapi.asterdex.com/fapi/v1/premiumIndex");
35
- if (!res.ok)
36
- throw new Error(`Aster premiumIndex HTTP ${res.status}`);
37
- const premiums = await res.json();
38
- if (!Array.isArray(premiums))
39
- throw new Error("Aster premiumIndex returned a non-array response");
40
- const out = [];
41
- for (const p of premiums) {
42
- const rawSym = String(p.symbol ?? "");
43
- if (!rawSym.endsWith("USDT"))
44
- continue;
45
- const fundingRate = Number(p.lastFundingRate);
46
- const markPrice = Number(p.markPrice);
47
- if (!Number.isFinite(fundingRate) || !Number.isFinite(markPrice) || markPrice <= 0) {
48
- continue;
49
- }
50
- out.push({
51
- exchange: "aster",
52
- symbol: rawSym.replace(/USDT$/, ""),
53
- fundingRate,
54
- markPrice,
55
- });
56
- }
57
- return out;
58
- }
59
- async function fetchLighterRates() {
60
- // SSOT rule #2: error must propagate. Caller (handleBasisScan) uses
61
- // Promise.allSettled to keep multi-DEX comparison alive when one DEX is
62
- // down, with explicit stderr logging.
63
- const [details, funding] = await Promise.all([
64
- fetchLighterOrderBookDetails(),
65
- fetchLighterFundingRates(),
66
- ]);
67
- const priceMap = new Map(details.map(d => [d.marketId, d.lastTradePrice]));
68
- const symMap = new Map(details.map(d => [d.marketId, d.symbol]));
69
- const rates = [];
70
- for (const fr of funding) {
71
- const symbol = fr.symbol || symMap.get(fr.marketId);
72
- if (!symbol)
73
- continue;
74
- // Prefer fr.markPrice; fall back to orderBook last-trade as documented
75
- // price-source preference (NOT error fallback). Skip if neither resolves
76
- // — never publish a 0 markPrice downstream.
77
- const directMark = fr.markPrice;
78
- const fallbackMark = priceMap.get(fr.marketId);
79
- let markPrice;
80
- if (directMark !== null && Number.isFinite(directMark) && directMark > 0) {
81
- markPrice = directMark;
82
- }
83
- else if (fallbackMark !== undefined && Number.isFinite(fallbackMark) && fallbackMark > 0) {
84
- markPrice = fallbackMark;
85
- }
86
- if (markPrice === undefined || !Number.isFinite(fr.rate))
87
- continue;
88
- rates.push({
89
- exchange: "lighter",
90
- symbol,
91
- fundingRate: fr.rate,
92
- markPrice,
93
- });
94
- }
95
- return rates;
96
- }
97
11
  // annualize moved to ../funding.ts — use annualizeRate() / computeAnnualSpread()
98
12
  export function registerArbCommands(program, isJson, getAdapterForExchange) {
99
13
  const arb = program.command("arb").description("Funding rate arbitrage & basis trading");
@@ -291,12 +205,15 @@ function printSnapshotTable(snapshot) {
291
205
  return r ? formatPercent(r.fundingRate) : chalk.gray("-");
292
206
  }),
293
207
  spreadColor(`${s.maxSpreadAnnual.toFixed(1)}%`), trend,
294
- s.maxSpreadAnnual >= 5 ? `${exAbbr(s.shortExchange)}>${exAbbr(s.longExchange)}` : chalk.gray("-"),
208
+ s.maxSpreadAnnual >= 5 ? `${exAbbr(s.shortExchange)}>${exAbbr(s.longExchange)}${s.executable ? "" : chalk.gray(" (read-only)")}` : chalk.gray("-"),
295
209
  formatAvgRate(bestRate?.historicalAvg?.avg8h), formatAvgRate(bestRate?.historicalAvg?.avg24h), formatAvgRate(bestRate?.historicalAvg?.avg7d),
296
210
  ];
297
211
  });
298
- console.log(makeTable(["Symbol", ...columns.map(v => v.displayName), "Spread", "24h", "Direction", "Avg 8h", "Avg 24h", "Avg 7d"], rows));
212
+ console.log(makeTable(["Symbol", ...columns.map(v => v.apiStatus.kind === "unofficial" ? `${v.displayName}*` : v.displayName), "Spread", "24h", "Direction", "Avg 8h", "Avg 24h", "Avg 7d"], rows));
299
213
  console.log(chalk.gray(`\n ${snapshot.symbols.length} symbols compared across exchanges.\n`));
214
+ const unofficial = columns.filter(v => v.apiStatus.kind === "unofficial");
215
+ if (unofficial.length > 0)
216
+ console.log(chalk.yellow(` * ${unofficial.map(v => v.displayName).join(", ")}: UNOFFICIAL API (undocumented, read-only; may change or break). Unverified funding — shown only with --include-unverified.\n`));
300
217
  }
301
218
  function printFundingExchangeStatus(snapshot) {
302
219
  const statuses = Object.entries(snapshot.exchangeStatus).map(([ex, status]) => {
@@ -317,7 +234,7 @@ function printDetailedComparison(comparison) {
317
234
  }
318
235
  const spreadColor = comparison.maxSpreadAnnual >= 30 ? chalk.green.bold : comparison.maxSpreadAnnual >= 10 ? chalk.yellow : chalk.white;
319
236
  console.log(`\n Max Spread: ${spreadColor(`${comparison.maxSpreadAnnual.toFixed(1)}%`)} annual`);
320
- console.log(` Direction: Long ${exAbbr(comparison.longExchange)} / Short ${exAbbr(comparison.shortExchange)}`);
237
+ console.log(` Direction: Long ${exAbbr(comparison.longExchange)} / Short ${exAbbr(comparison.shortExchange)}${comparison.executable ? "" : chalk.yellow(` — not executable: ${comparison.readOnlyLegs.join(", ")} is read-only in perp-cli`)}`);
321
238
  console.log(` Est. Income: $${comparison.estHourlyIncomeUsd.toFixed(4)}/hr per $1K notional\n`);
322
239
  }
323
240
  // registerFundingSubcommands removed — use 'arb scan --rates/--compare/--history/--positions' instead
@@ -325,9 +242,13 @@ function printDetailedComparison(comparison) {
325
242
  /** One-line stderr notice when rows of unverified-funding venues are shown (--include-unverified). */
326
243
  function warnUnverifiedFunding(rates) {
327
244
  const venues = [...new Set(rates.filter(r => !r.fundingVerified).map(r => r.exchange))];
328
- if (venues.length === 0)
329
- return;
330
- process.stderr.write(`[warn] ${venues.join(", ")}: funding rates are UNVERIFIED (unit not confirmed against funding payments; annualized values overstate venues whose funding accrues only during market hours). Shown on request — not for trading decisions.\n`);
245
+ if (venues.length > 0) {
246
+ process.stderr.write(`[warn] ${venues.join(", ")}: funding rates are UNVERIFIED (unit not confirmed against funding payments; annualized values overstate venues whose funding accrues only during market hours). Shown on request — not for trading decisions.\n`);
247
+ }
248
+ const unofficial = [...new Set(rates.filter(r => r.apiStatus === "unofficial").map(r => r.exchange))];
249
+ if (unofficial.length > 0) {
250
+ process.stderr.write(`[warn] ${unofficial.join(", ")}: read through an UNOFFICIAL API (undocumented; may change or be blocked without notice). Rows carry apiStatus "unofficial".\n`);
251
+ }
331
252
  }
332
253
  export async function handleRates(isJson, opts) {
333
254
  const minSpread = parseFloat(opts.minSpread);
@@ -373,59 +294,52 @@ export async function handleRates(isJson, opts) {
373
294
  printSnapshotTable(snapshot);
374
295
  printFundingExchangeStatus(snapshot);
375
296
  }
297
+ /**
298
+ * Cross-venue price basis — a PRICE comparison, independent of funding.
299
+ *
300
+ * Venue scope: every venue with the "trading" role (both legs must be executable), whether or not
301
+ * its funding rate is verified; market-data-only venues (TxFlow, unofficial API) are out. Prices
302
+ * come from each venue's market directory and are grouped by `canonical` symbol with the funding
303
+ * scan's guards (fetchCanonicalPriceGroups): unclassified / ambiguous tickers have no canonical and
304
+ * never pair, and a venue whose mark deviates more than MAX_PRICE_DEVIATION_PCT from the group
305
+ * median is dropped. The chosen cheap / rich pair must itself agree within MAX_PRICE_DEVIATION_PCT:
306
+ * a wider gap is a different asset under the same ticker (BB on Lighter 8.50 vs Aster 0.0091).
307
+ * A venue that fails is reported on stderr and is absent from the scan, never silently empty.
308
+ */
376
309
  export async function handleBasisScan(isJson, opts) {
377
310
  if (!isJson())
378
311
  console.log(chalk.cyan(" Fetching prices for basis calculation...\n"));
379
- // SSOT rule #2: each DEX is independent. allSettled keeps the comparison
380
- // alive when one DEX errors out, while rejection reasons get logged to
381
- // stderr (explicit failure, never silent [] downgrade).
382
- const settled = await Promise.allSettled([
383
- fetchPacificaRates(),
384
- fetchHyperliquidRates(),
385
- fetchLighterRates(),
386
- fetchAsterRates(),
387
- ]);
388
- logSettledRejections(settled, ["pacifica", "hyperliquid", "lighter", "aster"], "basis-scan");
389
- const pacRates = settled[0].status === "fulfilled" ? settled[0].value : [];
390
- const hlRates = settled[1].status === "fulfilled" ? settled[1].value : [];
391
- const ltRates = settled[2].status === "fulfilled" ? settled[2].value : [];
392
- const astRates = settled[3].status === "fulfilled" ? settled[3].value : [];
393
- const exchangePrices = new Map();
394
312
  const filterSymbol = opts.symbol?.toUpperCase();
395
- for (const r of [...pacRates, ...hlRates, ...ltRates, ...astRates]) {
396
- if (r.markPrice <= 0)
397
- continue;
398
- if (filterSymbol && r.symbol.toUpperCase() !== filterSymbol)
399
- continue;
400
- if (!exchangePrices.has(r.symbol))
401
- exchangePrices.set(r.symbol, new Map());
402
- exchangePrices.get(r.symbol).set(r.exchange, r.markPrice);
403
- }
313
+ const snapshot = await fetchCanonicalPriceGroups(listVenuesWithRole("trading"), {
314
+ symbols: filterSymbol !== undefined ? [filterSymbol] : undefined, tag: "basis-scan",
315
+ });
316
+ for (const [venue, msg] of Object.entries(snapshot.exchangeErrors))
317
+ console.error(`[basis-scan] ${venue} fetch failed: ${msg}`);
404
318
  const opps = [];
405
- for (const [symbol, prices] of exchangePrices.entries()) {
406
- if (prices.size < 2)
319
+ for (const [symbol, priced] of snapshot.groups) {
320
+ if (priced.length < 2)
407
321
  continue;
408
- let minEx = "", maxEx = "", minPrice = Infinity, maxPrice = 0;
409
- for (const [ex, price] of prices) {
410
- if (price < minPrice) {
411
- minPrice = price;
412
- minEx = ex;
413
- }
414
- if (price > maxPrice) {
415
- maxPrice = price;
416
- maxEx = ex;
417
- }
322
+ let min = priced[0], max = priced[0];
323
+ for (const r of priced) {
324
+ if (r.markPrice < min.markPrice)
325
+ min = r;
326
+ if (r.markPrice > max.markPrice)
327
+ max = r;
418
328
  }
419
- if (minEx === maxEx)
329
+ if (min.exchange === max.exchange)
420
330
  continue;
421
- const diff = maxPrice - minPrice;
422
- const avgPrice = (maxPrice + minPrice) / 2;
331
+ const diff = max.markPrice - min.markPrice;
332
+ const avgPrice = (max.markPrice + min.markPrice) / 2;
423
333
  const pctDiff = (diff / avgPrice) * 100;
334
+ if (!(pctDiff <= MAX_PRICE_DEVIATION_PCT)) {
335
+ console.error(`[basis-scan] excluded ${symbol} ${min.exchange}↔${max.exchange}: mark gap ${pctDiff.toFixed(1)}% > ${MAX_PRICE_DEVIATION_PCT}% (likely a different asset with the same ticker)`);
336
+ continue;
337
+ }
424
338
  if (pctDiff * 365 >= parseFloat(opts.minBasis)) {
425
339
  const priceObj = {};
426
- for (const [ex, p] of prices)
427
- priceObj[ex] = p;
428
- opps.push({ symbol, prices: priceObj, priceDiff: diff, pctDiff, buyOn: minEx, sellOn: maxEx });
340
+ for (const r of priced)
341
+ priceObj[r.exchange] = r.markPrice;
342
+ opps.push({ symbol, prices: priceObj, priceDiff: diff, pctDiff, buyOn: min.exchange, sellOn: max.exchange });
429
343
  }
430
344
  }
431
345
  opps.sort((a, b) => b.pctDiff - a.pctDiff);
@@ -439,12 +353,12 @@ export async function handleBasisScan(isJson, opts) {
439
353
  console.log(chalk.gray(" Strategy: Buy cheap, sell expensive, profit from convergence.\n"));
440
354
  const rows = opps.map((o) => [
441
355
  chalk.white.bold(o.symbol),
442
- o.prices.pacifica ? `$${formatUsd(o.prices.pacifica)}` : chalk.gray("-"),
443
- o.prices.hyperliquid ? `$${formatUsd(o.prices.hyperliquid)}` : chalk.gray("-"),
444
- o.prices.lighter ? `$${formatUsd(o.prices.lighter)}` : chalk.gray("-"),
356
+ `$${formatUsd(o.prices[o.buyOn])}`,
357
+ `$${formatUsd(o.prices[o.sellOn])}`,
445
358
  `$${formatUsd(o.priceDiff)}`, `${o.pctDiff.toFixed(4)}%`, chalk.green(o.buyOn), chalk.red(o.sellOn),
359
+ chalk.gray(String(Object.keys(o.prices).length)),
446
360
  ]);
447
- console.log(makeTable(["Symbol", "Pacifica", "Hyperliquid", "Lighter", "Diff", "Basis %", "Buy On", "Sell On"], rows));
361
+ console.log(makeTable(["Symbol", "Buy Price", "Sell Price", "Diff", "Basis %", "Buy On", "Sell On", "Venues"], rows));
448
362
  }
449
363
  export async function handleDexScan(isJson, opts) {
450
364
  if (!isJson())
@@ -497,9 +411,13 @@ export async function handleCompare(isJson, symbol, includeUnverified = false) {
497
411
  if (comparison)
498
412
  warnUnverifiedFunding(comparison.rates);
499
413
  if (!comparison) {
414
+ // A venue ticker whose canonical symbol differs (Variational US500-PERP = SPY): say where to look.
415
+ const hint = (await import("../exchanges/variational/public.js")).canonicalHintFor(symbol);
500
416
  if (isJson())
501
- return printJson(jsonOk({ symbol: symbol.toUpperCase(), available: false }));
417
+ return printJson(jsonOk({ symbol: symbol.toUpperCase(), available: false, ...(hint !== null ? { hint } : {}) }));
502
418
  console.log(chalk.gray(` ${symbol.toUpperCase()} not found on at least 2 exchanges.\n`));
419
+ if (hint !== null)
420
+ console.log(chalk.yellow(` Hint: ${hint}\n`));
503
421
  return;
504
422
  }
505
423
  if (isJson())
@@ -594,7 +512,7 @@ export async function handleFundingPositions(isJson, getAdapterForExchange, opts
594
512
  }
595
513
  const sz = Math.abs(Number(pos.size));
596
514
  const nu = sz * mk;
597
- const fh = fd ? fd.fundingHours : null; /* market basis first (§10.1) */
515
+ const fh = fd ? fd.fundingHours : null; /* market basis first */
598
516
  const hr = fh !== null ? fr / fh : toHourlyRate(fr, exchange);
599
517
  const ap = annualizeHourlyRate(hr);
600
518
  const hp = estimateHourlyFundingFromHourly(hr, nu, pos.side);
@@ -419,7 +419,7 @@ export function registerStrategyCommands(program, getAdapter, getAdapterFor, isJ
419
419
  }
420
420
  console.log();
421
421
  });
422
- // ── quarantine (design §12.3): positions the bot refuses to touch until a human clears them ──
422
+ // ── quarantine: positions the bot refuses to touch until a human clears them ──
423
423
  bot
424
424
  .command("quarantine")
425
425
  .description("List quarantined symbols (unknown / unverified order outcome or unhedged leg) — blocked for every bot strategy, arb exec and arb auto (~/.perp/bot-quarantine.json)")
@@ -283,7 +283,7 @@ export function registerWalletBuilderCommands(wallet, isJson, getAdapterForExcha
283
283
  .action(async (venueArg, _opts, cmd) => {
284
284
  await withJsonErrors(isJson(), async () => {
285
285
  const network = networkOf(cmd);
286
- const venues = venueArg !== undefined ? [getVenue(venueArg).id] : [...builderFeeVenues(), ...["lighter", "lighter-rh", "arcus"]];
286
+ const venues = venueArg !== undefined ? [getVenue(venueArg).id] : [...builderFeeVenues(), ...["lighter", "lighter-rh", "arcus", "risex", "txflow", "variational", "grvt", "extended"]];
287
287
  const rows = [];
288
288
  for (const v of venues) {
289
289
  const configured = builderTargetOf(loadSettings().builder, v) !== null;
@@ -7,7 +7,10 @@ import { registerFundsBridgeCommands } from "./bridge.js";
7
7
  import { registerFundsRebalanceCommands } from "./rebalance.js";
8
8
  import { PerpError, isConnectFailure, isDefiniteOrderFailure, isTransportFailure, orderOutcomeUnknown, parsePositiveDecimal } from "../errors.js";
9
9
  import { cliCredentialOptsFrom, requireMasterWallet } from "../config.js";
10
- import { getVenue, requireCapabilities, READ_ONLY_CREDENTIALS } from "../exchanges/venue.js";
10
+ import { getVenue, requireCapabilities, roleUnsupported, READ_ONLY_CREDENTIALS } from "../exchanges/venue.js";
11
+ import { RISEX_APP_URL, RISEX_FUNDS_APP_ONLY } from "../exchanges/venues/risex.js";
12
+ import { VARIATIONAL_READ_ONLY_REMEDIATION } from "../exchanges/venues/variational.js";
13
+ import { EXTENDED_READ_ONLY_REMEDIATION } from "../exchanges/venues/extended.js";
11
14
  import { unsupported } from "../exchanges/trading-v2.js";
12
15
  import { registerOnchainFundsCommands } from "./funds-onchain.js";
13
16
  import { QFEX_BANKER_NETWORK, QFEX_WITHDRAW_DISABLED, QFEX_WITHDRAW_ENABLED, QFEX_WITHDRAW_NETWORK } from "../exchanges/venues/qfex.js";
@@ -857,6 +860,34 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
857
860
  throw err;
858
861
  }
859
862
  });
863
+ supportsDryRun(faucet.command("risex"))
864
+ .description("[testnet tooling] Get the RISEx testnet faucet's 1000 test USDC (once per wallet; also registers the account; refused on mainnet; supports --dry-run). Refused while RISEx is read-only (trading in a later release)")
865
+ .action(async () => {
866
+ // Gated with RISEx trading (read-only release): the faucet is a venue write.
867
+ const risexDef = getVenue("risex");
868
+ if (!risexDef.roles.includes("trading"))
869
+ throw roleUnsupported(risexDef, "faucet");
870
+ const network = getNetwork();
871
+ if (network !== "testnet") {
872
+ throw unsupported("RISEx", `the test-USDC faucet on ${network}`, "the faucet exists only on testnet", "Run on testnet: perp -n testnet funds faucet risex");
873
+ }
874
+ const adapter = await getAdapterForExchange("risex");
875
+ const { RisexAdapter } = await import("../exchanges/risex/adapter.js");
876
+ if (!(adapter instanceof RisexAdapter))
877
+ throw new Error("risex venue did not produce a RisexAdapter");
878
+ const account = adapter.accountAddress();
879
+ if (isDryRun()) {
880
+ return printFundsPlan({ action: "deposit", venue: "risex", network, asset: "USDC (test)", amount: "1000", method: "testnet faucet POST /v1/account/deposit", account });
881
+ }
882
+ const res = await adapter.testnetFaucet();
883
+ logExecution({ type: "bridge", exchange: "risex", symbol: "USDC", side: "deposit", size: "1000", status: "success", dryRun: false, meta: { action: "faucet", network, account } });
884
+ if (isJson())
885
+ return printJson(jsonOk({ ...res, account, amount: "1000", network, method: "faucet" }));
886
+ console.log(chalk.green(`\n RISEx testnet faucet: 1000 test USDC requested for ${account}`));
887
+ if (res.txHash !== null)
888
+ console.log(` Tx: ${res.txHash}`);
889
+ console.log();
890
+ });
860
891
  // ═══════════════════════════════════════════════════════
861
892
  // WITHDRAW
862
893
  // ═══════════════════════════════════════════════════════
@@ -1049,7 +1080,7 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
1049
1080
  throw err;
1050
1081
  }
1051
1082
  });
1052
- // ── Arcus — uniform surface (design §13.1): same <amount> + --to,
1083
+ // ── Arcus — uniform surface: same <amount> + --to,
1053
1084
  // same jsonOk(result), global --dry-run plans without sending. ──
1054
1085
  // Ondo Perps: the API does not allow withdrawals (address-book destinations only; adding one is
1055
1086
  // refused for API keys). The command stays so the refusal is explicit — also under --dry-run.
@@ -1059,6 +1090,42 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
1059
1090
  .action(async () => {
1060
1091
  throw unsupported("Ondo Perps", "withdraw", "the Ondo Perps API does not allow withdrawals", "Withdraw from the Ondo Perps web app");
1061
1092
  });
1093
+ // RISEx: deposits / withdrawals are app-only (owner decision). The commands stay so the refusal
1094
+ // is explicit — also under --dry-run, before any credential or request.
1095
+ supportsDryRun(withdraw.command("risex [amount]"))
1096
+ .description(`Not available: RISEx withdrawals are done in the RISEx app (${RISEX_APP_URL}) — refused, also with --dry-run`)
1097
+ .option("--to <address>", "Ignored — perp-cli does not move RISEx funds")
1098
+ .action(async () => {
1099
+ throw unsupported("risex", "withdraw", RISEX_FUNDS_APP_ONLY.reason, RISEX_FUNDS_APP_ONLY.remediation);
1100
+ });
1101
+ // TxFlow: market data only (unofficial read-only API).
1102
+ supportsDryRun(withdraw.command("txflow [amount]"))
1103
+ .description("Not available: TxFlow is market data only in perp-cli (unofficial read-only API) — refused")
1104
+ .option("--to <address>", "Ignored")
1105
+ .action(async () => {
1106
+ throw roleUnsupported(getVenue("txflow"), "withdraw");
1107
+ });
1108
+ // Variational: read-only (market data) in perp-cli; no public trading API yet.
1109
+ supportsDryRun(withdraw.command("variational [amount]"))
1110
+ .description("Not available: Variational is read-only (market data) in perp-cli — refused")
1111
+ .option("--to <address>", "Ignored")
1112
+ .action(async () => {
1113
+ throw roleUnsupported(getVenue("variational"), "withdraw");
1114
+ });
1115
+ // GRVT: read-only (market data) in this version.
1116
+ supportsDryRun(withdraw.command("grvt [amount]"))
1117
+ .description("Not available: GRVT is read-only (market data) in this version — refused, also with --dry-run")
1118
+ .option("--to <address>", "Ignored")
1119
+ .action(async () => {
1120
+ throw roleUnsupported(getVenue("grvt"), "withdraw");
1121
+ });
1122
+ // Extended: market data only in this version.
1123
+ supportsDryRun(withdraw.command("extended [amount]"))
1124
+ .description("Not available: Extended is market data only in this version — refused, also with --dry-run")
1125
+ .option("--to <address>", "Ignored")
1126
+ .action(async () => {
1127
+ throw roleUnsupported(getVenue("extended"), "withdraw");
1128
+ });
1062
1129
  for (const venueId of ["arcus"]) {
1063
1130
  const def = getVenue(venueId);
1064
1131
  const asset = withdrawAssetOf(def);
@@ -1128,7 +1195,7 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
1128
1195
  }
1129
1196
  });
1130
1197
  }
1131
- // ── QFEX (design §14.6, §14.12 #8) — custodial; deposit = address display, withdraw = banker POST
1198
+ // ── QFEX — custodial; deposit = address display, withdraw = banker POST
1132
1199
  // gated by settings.withdraw.qfex inside QfexAdapter.withdraw(); switched off (QFEX_WITHDRAW_ENABLED). ──
1133
1200
  /** QFEX adapter for the active network — the default imported key, or the one named by --agent. */
1134
1201
  const qfexAdapterFor = async (agentName) => {
@@ -1206,6 +1273,31 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
1206
1273
  throw err;
1207
1274
  }
1208
1275
  });
1276
+ supportsDryRun(deposit.command("risex [amount]"))
1277
+ .description(`Not available: RISEx deposits are done in the RISEx app (${RISEX_APP_URL}) — refused, also with --dry-run`)
1278
+ .action(async () => {
1279
+ throw unsupported("risex", "deposit", RISEX_FUNDS_APP_ONLY.reason, RISEX_FUNDS_APP_ONLY.remediation);
1280
+ });
1281
+ supportsDryRun(deposit.command("txflow [amount]"))
1282
+ .description("Not available: TxFlow is market data only in perp-cli (unofficial read-only API) — refused")
1283
+ .action(async () => {
1284
+ throw roleUnsupported(getVenue("txflow"), "deposit");
1285
+ });
1286
+ supportsDryRun(deposit.command("variational [amount]"))
1287
+ .description("Not available: Variational is read-only (market data) in perp-cli — refused")
1288
+ .action(async () => {
1289
+ throw roleUnsupported(getVenue("variational"), "deposit");
1290
+ });
1291
+ supportsDryRun(deposit.command("grvt [amount]"))
1292
+ .description("Not available: GRVT is read-only (market data) in this version — refused, also with --dry-run")
1293
+ .action(async () => {
1294
+ throw roleUnsupported(getVenue("grvt"), "deposit");
1295
+ });
1296
+ supportsDryRun(deposit.command("extended [amount]"))
1297
+ .description("Not available: Extended is market data only in this version — refused, also with --dry-run")
1298
+ .action(async () => {
1299
+ throw roleUnsupported(getVenue("extended"), "deposit");
1300
+ });
1209
1301
  supportsDryRun(deposit.command("qfex"))
1210
1302
  .description("Show the QFEX deposit address for USDC on Arbitrum One (perp-cli sends nothing — transfer from your own wallet; supports --dry-run)")
1211
1303
  // --chain, not --network: the root -n/--network (mainnet | testnet) would swallow a subcommand --network.
@@ -1258,6 +1350,14 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
1258
1350
  if (isNaN(amountNum) || amountNum <= 0)
1259
1351
  throw new Error("Invalid amount");
1260
1352
  const exchangeOpt = program.opts().exchange;
1353
+ // -e names a venue this command cannot move funds on: refuse instead of transferring on Hyperliquid.
1354
+ if (exchangeOpt !== undefined) {
1355
+ const def = getVenue(exchangeOpt);
1356
+ if (!def.roles.includes("account"))
1357
+ throw roleUnsupported(def, "transfer");
1358
+ if (def.id === "risex")
1359
+ throw unsupported("risex", "transfer", RISEX_FUNDS_APP_ONLY.reason, RISEX_FUNDS_APP_ONLY.remediation);
1360
+ }
1261
1361
  if (exchangeOpt !== undefined && getVenue(exchangeOpt).id === "qfex") {
1262
1362
  if (isDryRun()) {
1263
1363
  return printFundsPlan({ action: "transfer", venue: "qfex", network: getNetwork(), asset: "USDC", amount, from: opts.from ?? "the key's account", destination });
@@ -1349,6 +1449,11 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
1349
1449
  qfex: { chain: "Arbitrum", token: "USDC", min: null, method: "address display only: perp funds deposit qfex, then send USDC from your wallet" },
1350
1450
  aster: { chain: "BNB Chain (default) / Arbitrum", token: "USDT (default) / USDC", min: null, gas: "BNB / Arbitrum ETH", method: "approve + vault depositFor(token, forAddress, amount, broker 1) (mainnet only)", command: "perp funds deposit aster <amount> [--chain bsc|arbitrum] [--asset USDT|USDC]" },
1351
1451
  ondo: { chain: "Arbitrum (default) / Ethereum", token: "USDC", min: null, gas: "ETH", method: "USDC transfer to the account's funding address (mainnet only)", command: "perp funds deposit ondo <amount> [--chain arbitrum|ethereum] [--probe <amount>]" },
1452
+ risex: { supported: false, note: `${RISEX_FUNDS_APP_ONLY.reason} — ${RISEX_FUNDS_APP_ONLY.remediation}`, testnet: getVenue("risex").roles.includes("trading") ? "perp -n testnet funds faucet risex (1000 test USDC, once)" : null },
1453
+ txflow: { supported: false, note: "TxFlow is market data only in perp-cli (unofficial read-only API)" },
1454
+ variational: { supported: false, note: VARIATIONAL_READ_ONLY_REMEDIATION },
1455
+ grvt: { supported: false, note: "GRVT is read-only (market data) in this version; trading is planned for a later release" },
1456
+ extended: { supported: false, note: EXTENDED_READ_ONLY_REMEDIATION },
1352
1457
  },
1353
1458
  withdraw: {
1354
1459
  pacifica: { chain: "Solana", token: "USDC", speed: "~10s", fee: "none" },
@@ -1360,6 +1465,11 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
1360
1465
  ? { chain: chainLabel(QFEX_WITHDRAW_NETWORK), token: "USDC", command: "perp funds withdraw qfex <amount> --to <allowlisted address>", gate: "settings withdraw.qfex (allowlist, maxPerTx, maxPerDay)", note: "needs a key with the \"Deposit and withdraw\" permission" }
1361
1466
  : { supported: false, note: `${QFEX_WITHDRAW_DISABLED.reason} — ${QFEX_WITHDRAW_DISABLED.remediation}` },
1362
1467
  arcus: { chain: "Robinhood Chain", token: "USDG", command: "perp funds withdraw arcus <amount>" },
1468
+ risex: { supported: false, note: `${RISEX_FUNDS_APP_ONLY.reason} — ${RISEX_FUNDS_APP_ONLY.remediation}` },
1469
+ txflow: { supported: false, note: "TxFlow is market data only in perp-cli (unofficial read-only API)" },
1470
+ variational: { supported: false, note: VARIATIONAL_READ_ONLY_REMEDIATION },
1471
+ grvt: { supported: false, note: "GRVT is read-only (market data) in this version; trading is planned for a later release" },
1472
+ extended: { supported: false, note: EXTENDED_READ_ONLY_REMEDIATION },
1363
1473
  aster: { chain: "BNB Chain (default) / Arbitrum", token: "USDT (default) / USDC", command: "perp funds withdraw aster <amount> [--chain bsc|arbitrum] [--asset USDT|USDC]", fee: "POST /fapi/v3/aster/user-withdraw-info (taken from the amount)", requires: "mainnet; API wallet approved with --can-withdraw --ip-whitelist; destination = the account's own address" },
1364
1474
  },
1365
1475
  }));
@@ -47,7 +47,7 @@ export interface SetupSettingsChange {
47
47
  export declare function applySetupSettings(change: SetupSettingsChange): Settings;
48
48
  /** Real follow-up commands for the configured venues (all exist in this CLI). */
49
49
  export declare function nextStepCommands(venues: VenueId[], network: Network): string[];
50
- /** Plain-language slippage explanation (design §13.2, §13.10 #3). */
50
+ /** Plain-language slippage explanation. */
51
51
  export declare function slippageExplanation(venues: VenueId[]): string[];
52
52
  export type AgentStepResult = {
53
53
  venue: VenueId;
@@ -9,8 +9,9 @@ import { PerpError, classifyError } from "../errors.js";
9
9
  import { jsonOk, jsonError, printJson } from "../utils.js";
10
10
  import { resolvePassphrase } from "../agent-wallet/passphrase.js";
11
11
  import { loadOws } from "../signer/ows-loader.js";
12
- import { getVenue, listVenuesWithRole, networkConfig, tryResolveVenueId } from "../exchanges/venue.js";
12
+ import { getVenue, listVenuesWithRole, networkConfig, roleUnsupported, tryResolveVenueId } from "../exchanges/venue.js";
13
13
  import "../exchanges/venues/index.js";
14
+ import { VARIATIONAL_READ_ONLY_REMEDIATION } from "../exchanges/venues/variational.js";
14
15
  import { maxDefaultSlippageBps, parseSlippageSettingBps } from "../exchanges/slippage.js";
15
16
  import { agentApproveUnsupportedReason, approveVenueAgent, assertLighterSlot, isLighterFamilyVenue, deprecatedPassphraseOption, printApproveOutcome, promptHidden, verifyVenueAgent, warnDeprecatedPassphrase, } from "./agent.js";
16
17
  import { importOwsWallet } from "./wallet.js";
@@ -127,7 +128,11 @@ export function setupVenues() {
127
128
  /** Resolve one venue id/alias for setup: must be a trading venue with an environment on `network`. */
128
129
  export function resolveSetupVenue(input, network) {
129
130
  const id = tryResolveVenueId(input.trim());
130
- if (id === null || !getVenue(id).roles.includes("trading")) {
131
+ // A registered venue without the trading role (release-gated RISEx, market-data-only TxFlow /
132
+ // Variational / GRVT) is a capability refusal, not an unknown name.
133
+ if (id !== null && !getVenue(id).roles.includes("trading"))
134
+ throw roleUnsupported(getVenue(id), "setup");
135
+ if (id === null) {
131
136
  throw new PerpError("INVALID_PARAMS", `Unknown trading venue "${input}"`, {
132
137
  remediation: `Use one of: ${setupVenues().map(v => v.id).join(", ")}`,
133
138
  });
@@ -177,7 +182,7 @@ export function applySetupSettings(change) {
177
182
  }
178
183
  /** Agent names already registered for `venue` on `network` in settings. */
179
184
  function registeredAgentNames(venue, network) {
180
- if (venue === "ondo" || venue === "arcus" || venue === "qfex") {
185
+ if (venue === "ondo" || venue === "arcus" || venue === "qfex" || venue === "risex") {
181
186
  const map = loadSettings().agents?.[venue];
182
187
  return map ? Object.values(map).filter(m => m.network === network).map(m => m.agentName) : [];
183
188
  }
@@ -186,11 +191,11 @@ function registeredAgentNames(venue, network) {
186
191
  function rotateHint(venue) {
187
192
  if (venue === "qfex")
188
193
  return "perp wallet agent import qfex --replace";
189
- return venue === "ondo" || venue === "arcus"
194
+ return venue === "ondo" || venue === "arcus" || venue === "risex"
190
195
  ? `perp wallet agent approve ${venue} --rotate`
191
196
  : `perp wallet agent rotate ${venue}`;
192
197
  }
193
- /** Why the wizard cannot register an agent for `venue` on `network`; null when it can (uniform wording, design §13.6). */
198
+ /** Why the wizard cannot register an agent for `venue` on `network`; null when it can (uniform wording). */
194
199
  function approveBlocker(venue, network) {
195
200
  const reason = agentApproveUnsupportedReason(venue, network, "approve");
196
201
  return reason === null ? null : `${getVenue(venue).displayName} agent management on ${network} not supported — ${reason}`;
@@ -215,7 +220,7 @@ export function nextStepCommands(venues, network) {
215
220
  cmds.push("perp settings show");
216
221
  return cmds;
217
222
  }
218
- /** Plain-language slippage explanation (design §13.2, §13.10 #3). */
223
+ /** Plain-language slippage explanation. */
219
224
  export function slippageExplanation(venues) {
220
225
  const lines = [
221
226
  "Market orders are sent as immediate-or-cancel (IOC) limit orders. The limit price is the",
@@ -260,6 +265,19 @@ function signingSummary(p, master) {
260
265
  return ["No signature. Your Ondo API key is stored encrypted in ~/.perp/ondo-agents/ and checked with one read-only account request."];
261
266
  case "qfex":
262
267
  return ["No signature. Your QFEX API key is stored encrypted in ~/.perp/qfex-agents/ and checked with read-only requests."];
268
+ case "risex":
269
+ return [
270
+ `EIP-712 RegisterSigner signed by master EVM ${master.evm} (+ VerifySigner by the new signer): registers a session signer (${expiry}).`,
271
+ `Then the new signer signs DisablePermission for every permission except trading, and ${master.evm} sends those transactions ON-CHAIN on RISE chain (needs RISE-chain ETH for gas): the signer can trade but never move funds, and only the master could re-enable a permission.`,
272
+ ];
273
+ case "txflow":
274
+ return ["Nothing — TxFlow is market data only (unofficial read-only API)."];
275
+ case "variational":
276
+ return [`Nothing — ${VARIATIONAL_READ_ONLY_REMEDIATION}.`];
277
+ case "grvt":
278
+ return ["Nothing — GRVT is read-only (market data) in this version; trading is planned for a later release."];
279
+ case "extended":
280
+ return ["Nothing — Extended is read-only (market data) in this version; trading is planned for a later release."];
263
281
  }
264
282
  }
265
283
  function failed(venue, err) {
@@ -944,7 +962,9 @@ export async function runNonInteractiveSetup(opts) {
944
962
  let defaultVenue = null;
945
963
  if (defaultRaw !== undefined) {
946
964
  const id = tryResolveVenueId(defaultRaw);
947
- if (id === null || !getVenue(id).roles.includes("trading")) {
965
+ if (id !== null && !getVenue(id).roles.includes("trading"))
966
+ throw roleUnsupported(getVenue(id), "setup");
967
+ if (id === null) {
948
968
  throw new PerpError("INVALID_PARAMS", `Invalid --default-venue "${defaultRaw}"`, {
949
969
  remediation: `Use one of: ${(venues.length > 0 ? venues : setupVenues().map(v => v.id)).join(", ")}`,
950
970
  });