perp-cli 0.14.3 → 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 (311) 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 +2 -2
  23. package/dist/bot/strategies/funding-arb-utils.d.ts +1 -1
  24. package/dist/bot/strategies/funding-arb-utils.js +5 -5
  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 +2 -2
  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 +167 -35
  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/errors.d.ts +24 -1
  73. package/dist/errors.js +25 -3
  74. package/dist/exchanges/arcus/adapter.d.ts +7 -7
  75. package/dist/exchanges/arcus/adapter.js +12 -12
  76. package/dist/exchanges/arcus/agent.d.ts +1 -1
  77. package/dist/exchanges/arcus/agent.js +1 -1
  78. package/dist/exchanges/arcus/client.js +1 -1
  79. package/dist/exchanges/arcus/signing.d.ts +2 -3
  80. package/dist/exchanges/arcus/signing.js +1 -1
  81. package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
  82. package/dist/exchanges/arcus/spot-chain.js +1 -1
  83. package/dist/exchanges/arcus/spot-router.d.ts +1 -1
  84. package/dist/exchanges/arcus/spot-router.js +2 -2
  85. package/dist/exchanges/arcus/spot.d.ts +2 -3
  86. package/dist/exchanges/arcus/spot.js +2 -3
  87. package/dist/exchanges/aster-spot.d.ts +3 -3
  88. package/dist/exchanges/aster-spot.js +6 -9
  89. package/dist/exchanges/aster-typed-data.d.ts +1 -1
  90. package/dist/exchanges/aster-typed-data.js +1 -1
  91. package/dist/exchanges/aster.d.ts +4 -4
  92. package/dist/exchanges/aster.js +31 -13
  93. package/dist/exchanges/builder-fee.d.ts +1 -1
  94. package/dist/exchanges/builder-fee.js +13 -2
  95. package/dist/exchanges/canonical-symbols.d.ts +1 -2
  96. package/dist/exchanges/canonical-symbols.js +26 -2
  97. package/dist/exchanges/derive/client.d.ts +37 -0
  98. package/dist/exchanges/derive/client.js +135 -0
  99. package/dist/exchanges/derive/options.d.ts +68 -0
  100. package/dist/exchanges/derive/options.js +360 -0
  101. package/dist/exchanges/extended/adapter.d.ts +82 -0
  102. package/dist/exchanges/extended/adapter.js +179 -0
  103. package/dist/exchanges/extended/client.d.ts +38 -0
  104. package/dist/exchanges/extended/client.js +88 -0
  105. package/dist/exchanges/extended/public.d.ts +130 -0
  106. package/dist/exchanges/extended/public.js +429 -0
  107. package/dist/exchanges/grvt/adapter.d.ts +86 -0
  108. package/dist/exchanges/grvt/adapter.js +206 -0
  109. package/dist/exchanges/grvt/client.d.ts +25 -0
  110. package/dist/exchanges/grvt/client.js +86 -0
  111. package/dist/exchanges/grvt/public.d.ts +151 -0
  112. package/dist/exchanges/grvt/public.js +438 -0
  113. package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
  114. package/dist/exchanges/hyperliquid-spot.js +5 -5
  115. package/dist/exchanges/hyperliquid.d.ts +22 -12
  116. package/dist/exchanges/hyperliquid.js +80 -34
  117. package/dist/exchanges/index.d.ts +1 -1
  118. package/dist/exchanges/index.js +1 -1
  119. package/dist/exchanges/interface.d.ts +94 -6
  120. package/dist/exchanges/interface.js +16 -0
  121. package/dist/exchanges/ioc-outcome.d.ts +1 -1
  122. package/dist/exchanges/ioc-outcome.js +1 -1
  123. package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
  124. package/dist/exchanges/legacy-trading-v2.js +7 -2
  125. package/dist/exchanges/lighter-public.d.ts +2 -2
  126. package/dist/exchanges/lighter-public.js +2 -2
  127. package/dist/exchanges/lighter-signer.d.ts +1 -1
  128. package/dist/exchanges/lighter-signer.js +1 -1
  129. package/dist/exchanges/lighter-spot.d.ts +3 -4
  130. package/dist/exchanges/lighter-spot.js +6 -7
  131. package/dist/exchanges/lighter.d.ts +11 -11
  132. package/dist/exchanges/lighter.js +28 -14
  133. package/dist/exchanges/ondo/adapter.d.ts +5 -6
  134. package/dist/exchanges/ondo/adapter.js +16 -14
  135. package/dist/exchanges/ondo/agent.d.ts +1 -1
  136. package/dist/exchanges/ondo/agent.js +1 -1
  137. package/dist/exchanges/ondo/client.d.ts +1 -1
  138. package/dist/exchanges/ondo/client.js +2 -2
  139. package/dist/exchanges/ondo/public.d.ts +8 -1
  140. package/dist/exchanges/ondo/public.js +13 -2
  141. package/dist/exchanges/ondo/spot.d.ts +2 -2
  142. package/dist/exchanges/ondo/spot.js +3 -3
  143. package/dist/exchanges/options-interface.d.ts +158 -0
  144. package/dist/exchanges/options-interface.js +17 -0
  145. package/dist/exchanges/options-venues.d.ts +12 -0
  146. package/dist/exchanges/options-venues.js +35 -0
  147. package/dist/exchanges/pacifica-atomic.d.ts +211 -0
  148. package/dist/exchanges/pacifica-atomic.js +440 -0
  149. package/dist/exchanges/pacifica-decimal.d.ts +10 -0
  150. package/dist/exchanges/pacifica-decimal.js +51 -0
  151. package/dist/exchanges/pacifica-presigned.d.ts +53 -0
  152. package/dist/exchanges/pacifica-presigned.js +141 -0
  153. package/dist/exchanges/pacifica-print.d.ts +200 -0
  154. package/dist/exchanges/pacifica-print.js +417 -0
  155. package/dist/exchanges/pacifica-spot.d.ts +2 -2
  156. package/dist/exchanges/pacifica-spot.js +3 -3
  157. package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
  158. package/dist/exchanges/pacifica-typed-data.js +1 -1
  159. package/dist/exchanges/pacifica-vaults.d.ts +121 -0
  160. package/dist/exchanges/pacifica-vaults.js +264 -0
  161. package/dist/exchanges/pacifica.d.ts +32 -5
  162. package/dist/exchanges/pacifica.js +114 -48
  163. package/dist/exchanges/precision.d.ts +3 -1
  164. package/dist/exchanges/precision.js +9 -2
  165. package/dist/exchanges/qfex/adapter.d.ts +18 -5
  166. package/dist/exchanges/qfex/adapter.js +32 -12
  167. package/dist/exchanges/qfex/agent.d.ts +1 -1
  168. package/dist/exchanges/qfex/agent.js +1 -1
  169. package/dist/exchanges/qfex/client.d.ts +1 -1
  170. package/dist/exchanges/qfex/client.js +1 -1
  171. package/dist/exchanges/qfex/errors.d.ts +1 -1
  172. package/dist/exchanges/qfex/errors.js +2 -2
  173. package/dist/exchanges/qfex/public.d.ts +3 -3
  174. package/dist/exchanges/qfex/public.js +3 -3
  175. package/dist/exchanges/qfex/signing.d.ts +2 -2
  176. package/dist/exchanges/qfex/signing.js +4 -5
  177. package/dist/exchanges/qfex/ws-transport.d.ts +2 -2
  178. package/dist/exchanges/qfex/ws-transport.js +3 -3
  179. package/dist/exchanges/rate-limiter.d.ts +1 -1
  180. package/dist/exchanges/rate-limiter.js +5 -2
  181. package/dist/exchanges/registry.d.ts +1 -1
  182. package/dist/exchanges/registry.js +1 -1
  183. package/dist/exchanges/risex/adapter.d.ts +148 -0
  184. package/dist/exchanges/risex/adapter.js +796 -0
  185. package/dist/exchanges/risex/agent.d.ts +154 -0
  186. package/dist/exchanges/risex/agent.js +384 -0
  187. package/dist/exchanges/risex/client.d.ts +47 -0
  188. package/dist/exchanges/risex/client.js +174 -0
  189. package/dist/exchanges/risex/nonce.d.ts +43 -0
  190. package/dist/exchanges/risex/nonce.js +212 -0
  191. package/dist/exchanges/risex/public.d.ts +64 -0
  192. package/dist/exchanges/risex/public.js +150 -0
  193. package/dist/exchanges/risex/session.d.ts +71 -0
  194. package/dist/exchanges/risex/session.js +171 -0
  195. package/dist/exchanges/risex/typed-data.d.ts +173 -0
  196. package/dist/exchanges/risex/typed-data.js +240 -0
  197. package/dist/exchanges/risex/units.d.ts +29 -0
  198. package/dist/exchanges/risex/units.js +104 -0
  199. package/dist/exchanges/slippage.d.ts +1 -1
  200. package/dist/exchanges/slippage.js +1 -1
  201. package/dist/exchanges/spot-factory.d.ts +3 -3
  202. package/dist/exchanges/spot-factory.js +7 -7
  203. package/dist/exchanges/spot-interface.d.ts +5 -5
  204. package/dist/exchanges/spot-interface.js +2 -2
  205. package/dist/exchanges/spot-result.d.ts +3 -3
  206. package/dist/exchanges/spot-result.js +3 -3
  207. package/dist/exchanges/trading-v2.d.ts +3 -3
  208. package/dist/exchanges/trading-v2.js +9 -6
  209. package/dist/exchanges/txflow/adapter.d.ts +54 -0
  210. package/dist/exchanges/txflow/adapter.js +118 -0
  211. package/dist/exchanges/txflow/client.d.ts +47 -0
  212. package/dist/exchanges/txflow/client.js +153 -0
  213. package/dist/exchanges/txflow/public.d.ts +76 -0
  214. package/dist/exchanges/txflow/public.js +284 -0
  215. package/dist/exchanges/unknown-symbol.d.ts +29 -0
  216. package/dist/exchanges/unknown-symbol.js +51 -0
  217. package/dist/exchanges/variational/adapter.d.ts +71 -0
  218. package/dist/exchanges/variational/adapter.js +137 -0
  219. package/dist/exchanges/variational/client.d.ts +21 -0
  220. package/dist/exchanges/variational/client.js +66 -0
  221. package/dist/exchanges/variational/public.d.ts +126 -0
  222. package/dist/exchanges/variational/public.js +430 -0
  223. package/dist/exchanges/venue-types.d.ts +6 -7
  224. package/dist/exchanges/venue-types.js +2 -2
  225. package/dist/exchanges/venue.d.ts +51 -8
  226. package/dist/exchanges/venue.js +54 -4
  227. package/dist/exchanges/venues/arcus.d.ts +1 -2
  228. package/dist/exchanges/venues/arcus.js +5 -4
  229. package/dist/exchanges/venues/aster.d.ts +2 -2
  230. package/dist/exchanges/venues/aster.js +6 -4
  231. package/dist/exchanges/venues/create-helpers.d.ts +1 -1
  232. package/dist/exchanges/venues/create-helpers.js +3 -3
  233. package/dist/exchanges/venues/extended.d.ts +21 -0
  234. package/dist/exchanges/venues/extended.js +96 -0
  235. package/dist/exchanges/venues/grvt.d.ts +30 -0
  236. package/dist/exchanges/venues/grvt.js +98 -0
  237. package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
  238. package/dist/exchanges/venues/hyperliquid.js +10 -8
  239. package/dist/exchanges/venues/index.d.ts +7 -2
  240. package/dist/exchanges/venues/index.js +8 -2
  241. package/dist/exchanges/venues/lighter-family.d.ts +7 -9
  242. package/dist/exchanges/venues/lighter-family.js +9 -11
  243. package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
  244. package/dist/exchanges/venues/lighter-rh.js +7 -5
  245. package/dist/exchanges/venues/lighter.d.ts +1 -1
  246. package/dist/exchanges/venues/lighter.js +5 -3
  247. package/dist/exchanges/venues/ondo.d.ts +1 -1
  248. package/dist/exchanges/venues/ondo.js +5 -3
  249. package/dist/exchanges/venues/pacifica.d.ts +2 -2
  250. package/dist/exchanges/venues/pacifica.js +6 -4
  251. package/dist/exchanges/venues/qfex.d.ts +1 -1
  252. package/dist/exchanges/venues/qfex.js +4 -2
  253. package/dist/exchanges/venues/risex-release.d.ts +14 -0
  254. package/dist/exchanges/venues/risex-release.js +14 -0
  255. package/dist/exchanges/venues/risex.d.ts +31 -0
  256. package/dist/exchanges/venues/risex.js +116 -0
  257. package/dist/exchanges/venues/txflow.d.ts +18 -0
  258. package/dist/exchanges/venues/txflow.js +77 -0
  259. package/dist/exchanges/venues/variational.d.ts +21 -0
  260. package/dist/exchanges/venues/variational.js +85 -0
  261. package/dist/execution-log.d.ts +1 -1
  262. package/dist/funding/legacy-public.d.ts +1 -1
  263. package/dist/funding/legacy-public.js +9 -4
  264. package/dist/funding/normalize.d.ts +1 -1
  265. package/dist/funding/normalize.js +1 -1
  266. package/dist/funding/rates.d.ts +44 -4
  267. package/dist/funding/rates.js +56 -12
  268. package/dist/index.js +7 -6
  269. package/dist/jobs.d.ts +1 -0
  270. package/dist/jobs.js +25 -1
  271. package/dist/landing.d.ts +2 -0
  272. package/dist/landing.js +1 -1
  273. package/dist/mcp-pacifica-earn.d.ts +9 -0
  274. package/dist/mcp-pacifica-earn.js +58 -0
  275. package/dist/mcp-server.js +113 -59
  276. package/dist/pacifica/client.d.ts +67 -4
  277. package/dist/pacifica/client.js +170 -8
  278. package/dist/pacifica/rate-limit.d.ts +144 -0
  279. package/dist/pacifica/rate-limit.js +267 -0
  280. package/dist/pacifica/types/order.d.ts +2 -1
  281. package/dist/plan-executor.js +7 -2
  282. package/dist/rebalance.js +1 -1
  283. package/dist/risk.d.ts +2 -1
  284. package/dist/risk.js +2 -4
  285. package/dist/settings.d.ts +32 -3
  286. package/dist/signer/ows-evm.d.ts +1 -1
  287. package/dist/signer/ows-evm.js +1 -1
  288. package/dist/smart-order.d.ts +1 -1
  289. package/dist/smart-order.js +1 -1
  290. package/dist/strategies/funding-arb.d.ts +1 -1
  291. package/dist/strategies/funding-arb.js +1 -1
  292. package/dist/trade-validator.js +2 -2
  293. package/dist/utils.d.ts +12 -3
  294. package/dist/utils.js +11 -3
  295. package/dist/venue-health.d.ts +40 -0
  296. package/dist/venue-health.js +75 -0
  297. package/dist/withdraw-gate.d.ts +1 -1
  298. package/dist/withdraw-gate.js +1 -1
  299. package/dist/ws/index.d.ts +959 -0
  300. package/dist/ws/index.js +4295 -0
  301. package/dist/ws/node.d.ts +975 -0
  302. package/dist/ws/node.js +4366 -0
  303. package/dist/ws/pacifica-instrument.d.ts +8 -0
  304. package/dist/ws/pacifica-instrument.js +8 -0
  305. package/dist/ws-cli/credentials.d.ts +62 -0
  306. package/dist/ws-cli/credentials.js +126 -0
  307. package/package.json +58 -10
  308. package/skills/perp-cli/SKILL.md +62 -9
  309. package/skills/perp-cli/references/agent-operations.md +7 -0
  310. package/skills/perp-cli/references/commands.md +76 -5
  311. package/skills/perp-cli/references/ws-library.md +492 -0
@@ -20,7 +20,7 @@ import { loadArbState, addPosition as persistAddPosition, removePosition as pers
20
20
  import { handleRates, handleBasisScan, handleDexScan, handleCompare, handleFundingHistory, handleFundingPositions, fetchAllPrices, printGapTable, printTrackSummary, formatGapPrice, } from "./arb.js";
21
21
  // ── Fee-Adjusted Net Spread Calculation ──
22
22
  import { getTakerFee } from "../constants.js";
23
- import { assertFundingArbVenue, getVenue, listVenuesWithRole, networkConfig, resolveVenueId, tryResolveVenueId } from "../exchanges/venue.js";
23
+ import { assertFundingArbVenue, getVenue, listVenuesWithRole, networkConfig, resolveVenueId, roleUnsupported, tryResolveVenueId } from "../exchanges/venue.js";
24
24
  import { sharedApiNetwork } from "../api/public/urls.js";
25
25
  import { findSpotQuoteBalance, readSpotQuoteAvailableFresh, requireBotSpotVenue, requireSpotAdapter, spotNetworkOf, spotPairSymbol, spotSharesPerpMargin, sweepSpotQuoteToPerp, transferQuoteToPerp, transferQuoteToSpot } from "../exchanges/spot-factory.js";
26
26
  import { fillSizeString, readSpotBalancesFresh, sizeDecimalsOf, spotLotStepOf, spotMarketFill } from "../exchanges/spot-result.js";
@@ -821,7 +821,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
821
821
  if (stop.requested())
822
822
  break; // stopping: no new close sequence
823
823
  const pos = openPositions[i];
824
- // Quarantined (design §12.3): hands off until a human clears it.
824
+ // Quarantined: hands off until a human clears it.
825
825
  if (isQuarantined(pos.symbol, (m) => console.log(chalk.red(m))))
826
826
  continue;
827
827
  // Per-position isolation: one position's failing exit check (e.g. an
@@ -877,7 +877,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
877
877
  try {
878
878
  const longAdapter = await getAdapterForExchange(pos.longExchange);
879
879
  const shortAdapter = await getAdapterForExchange(pos.shortExchange);
880
- // Sequential, reduce-only, verified (design §12.3): long first, then
880
+ // Sequential, reduce-only, verified: long first, then
881
881
  // short. A definite long-close failure changes nothing → retried next
882
882
  // cycle, quarantined after MAX_PAIR_CLOSE_FAILURES in a row. An unknown
883
883
  // outcome, a partial close or an unhedged leg quarantines (no retry).
@@ -1114,7 +1114,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1114
1114
  skip(snap.symbol, "already open");
1115
1115
  continue;
1116
1116
  }
1117
- // Global quarantine (design §12.3): unknown / unhedged outcome on this symbol somewhere.
1117
+ // Global quarantine: unknown / unhedged outcome on this symbol somewhere.
1118
1118
  if (isQuarantined(snap.symbol, (m) => console.log(chalk.red(m)))) {
1119
1119
  skip(snap.symbol, "quarantined");
1120
1120
  continue;
@@ -1234,7 +1234,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1234
1234
  try {
1235
1235
  const longAdapter = await getAdapterForExchange(longExchange);
1236
1236
  const shortAdapter = await getAdapterForExchange(shortExchange);
1237
- // Sequential verified legs (design §12.3): long, then short sized to the
1237
+ // Sequential verified legs: long, then short sized to the
1238
1238
  // long's verified fill; reduce-only rollback / trim; unknown → quarantine.
1239
1239
  legsStarted = true;
1240
1240
  const res = await openPerpPairVerified({
@@ -1445,7 +1445,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1445
1445
  .option("--duration <minutes>", "Track duration in minutes (with --track)", "10")
1446
1446
  .option("--beep", "Beep on large gaps (with --gaps)")
1447
1447
  .option("--check-liquidity", "Check orderbook depth in live mode (slower)")
1448
- .option("--include-unverified", "With --rates / --compare: also show venues whose funding rate is unverified (qfex) — display only, never ranked for trading")
1448
+ .option("--include-unverified", "With --rates / --compare: also show venues whose funding rate is unverified (qfex; txflow — unofficial API; variational; grvt — indicative rate; extended) — display only, never ranked for trading")
1449
1449
  .action(async (opts) => {
1450
1450
  // ── Price gap modes ──
1451
1451
  if (opts.gaps || opts.track || opts.alert) {
@@ -1857,6 +1857,8 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1857
1857
  for (const ex of [longExch, shortExch]) {
1858
1858
  const venue = getVenue(ex);
1859
1859
  if (!venue.roles.includes("trading")) {
1860
+ if (venue.gatedRoles !== null)
1861
+ throw roleUnsupported(venue, "trading");
1860
1862
  throw unsupported(venue.displayName, "trading", "no trading API — it cannot be an arb leg", `Use a trading venue: ${listVenuesWithRole("trading").map(v => v.id).join(", ")}`);
1861
1863
  }
1862
1864
  }
@@ -1867,7 +1869,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1867
1869
  console.log(chalk.red(" Long and short exchange must be different."));
1868
1870
  return;
1869
1871
  }
1870
- // Global quarantine (design §12.3): an unknown / unhedged outcome on this symbol
1872
+ // Global quarantine: an unknown / unhedged outcome on this symbol
1871
1873
  // anywhere blocks new trades until a human clears it.
1872
1874
  const quarantined = quarantineEntryFor(sym, (m) => console.error(m));
1873
1875
  if (quarantined !== null) {
@@ -1875,7 +1877,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1875
1877
  remediation: `${quarantined.remediation} — then: perp strategy quarantine-clear ${sym}`,
1876
1878
  });
1877
1879
  }
1878
- // A venue whose funding rate is unverified is never a funding-arb leg (design §14.12 #6).
1880
+ // A venue whose funding rate is unverified is never a funding-arb leg.
1879
1881
  assertFundingArbVenue(longExch);
1880
1882
  assertFundingArbVenue(shortExch);
1881
1883
  const longAdapter = await getAdapterForExchange(longExch);
@@ -1991,7 +1993,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1991
1993
  }
1992
1994
  if (!isJson())
1993
1995
  console.log(chalk.gray(` Executing legs sequentially (long, then short sized to the verified long fill)...\n`));
1994
- // 5. Sequential verified legs (design §12.3). The second leg is sized to the
1996
+ // 5. Sequential verified legs. The second leg is sized to the
1995
1997
  // first leg's VERIFIED fill, any shortfall is trimmed off the long
1996
1998
  // (reduce-only, verified — always, --reconcile is implied), a definite short
1997
1999
  // failure rolls the long back reduce-only, and an unknown outcome (incl. a
@@ -2070,12 +2072,14 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
2070
2072
  // Resolve exchange aliases
2071
2073
  spotExch = resolveVenueId(spotExch);
2072
2074
  perpExch = resolveVenueId(perpExch);
2073
- // Validate spot exchange: bot/arb spot scope (§12.4 #3) — throws CAPABILITY_UNSUPPORTED
2075
+ // Validate spot exchange: bot/arb spot scope — throws CAPABILITY_UNSUPPORTED
2074
2076
  // for venues without spot or outside the scope (pacifica shared margin, arcus swaps).
2075
2077
  const spotDef = requireBotSpotVenue(spotExch.replace(/-spot$/, ""));
2076
2078
  spotExch = spotDef.id;
2077
2079
  perpExch = tryResolveVenueId(perpExch) ?? perpExch;
2078
2080
  if (!getVenue(perpExch).roles.includes("trading")) {
2081
+ if (getVenue(perpExch).gatedRoles !== null)
2082
+ throw roleUnsupported(getVenue(perpExch), "trading");
2079
2083
  throw unsupported(perpExch, "trading", "no trading API — it cannot be the perp leg", `Use a trading venue: ${listVenuesWithRole("trading").map(v => v.id).join(", ")}`);
2080
2084
  }
2081
2085
  assertFundingArbVenue(perpExch);
@@ -2093,7 +2097,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
2093
2097
  return;
2094
2098
  console.log(chalk.red(` ${msg}`));
2095
2099
  };
2096
- // Global quarantine (design §12.3): checked before anything is read or moved.
2100
+ // Global quarantine: checked before anything is read or moved.
2097
2101
  const quarantined = quarantineEntryFor(sym, (m) => console.error(m));
2098
2102
  if (quarantined !== null) {
2099
2103
  throw new PerpError("RISK_VIOLATION", `${sym} is quarantined (${quarantined.strategy}: ${quarantined.reason})`, {
@@ -2273,7 +2277,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
2273
2277
  // 7. Execute both legs. Same exchange shares a nonce → sequential; else parallel.
2274
2278
  if (!isJson())
2275
2279
  console.log(chalk.gray(` Executing ${sameVenue ? "sequentially" : "both legs simultaneously"}...\n`));
2276
- // The perp leg goes through placePerpVerified (design §12.3): a fill is the measured
2280
+ // The perp leg goes through placePerpVerified: a fill is the measured
2277
2281
  // position delta; an unknown outcome is ORDER_PENDING (quarantine, never roll back).
2278
2282
  let spotResult;
2279
2283
  let perpResult;
@@ -2523,7 +2527,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
2523
2527
  remediation: `Pass both: perp arb close ${sym} --spot-exch <spot venue> --perp-exch <perp venue>`,
2524
2528
  });
2525
2529
  }
2526
- // Load adapters (bot/arb spot scope — §12.4 #3)
2530
+ // Load adapters (bot/arb spot scope)
2527
2531
  const spotDef = requireBotSpotVenue(spotExch.replace(/-spot$/, ""));
2528
2532
  const spotAdapter = await requireSpotAdapter(spotDef.id, await getAdapter(spotDef.id));
2529
2533
  const perpAdapter = await getAdapter(perpExch);
@@ -2551,12 +2555,12 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
2551
2555
  // Read before anything moves — a failure aborts the close.
2552
2556
  const preCloseQuote = await readSpotQuoteAvailableFresh(spotAdapter);
2553
2557
  // Same exchange → sequential (nonce collision), different → parallel.
2554
- // The spot leg's REAL fill decides the outcome (§12.4 #5): only a confirmed spot sale
2558
+ // The spot leg's REAL fill decides the outcome: only a confirmed spot sale
2555
2559
  // counts as closed; legs are re-matched so no naked short / unhedged spot is left.
2556
2560
  const sameVenue = spotDef.id === (tryResolveVenueId(perpExch) ?? perpExch);
2557
2561
  const closeDec = sizeDecimalsOf(closeSize);
2558
- // The perp buy-back and every re-match order go through placePerpVerified (design
2559
- // §12.3): only a measured position change counts; an unknown outcome stops the close.
2562
+ // The perp buy-back and every re-match order go through placePerpVerified:
2563
+ // only a measured position change counts; an unknown outcome stops the close.
2560
2564
  let spotResult;
2561
2565
  let perpResult;
2562
2566
  if (sameVenue) {
@@ -3073,7 +3077,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
3073
3077
  (p.long.dex === pos.shortDex && p.short.dex === pos.longDex)));
3074
3078
  const currentSpread = current?.annualSpread ?? 0;
3075
3079
  if (currentSpread <= closeSpread || !current) {
3076
- // Quarantined (design §12.3): hands off until a human clears it.
3080
+ // Quarantined: hands off until a human clears it.
3077
3081
  if (isQuarantined(pos.underlying, (m) => console.error(chalk.red(m))))
3078
3082
  continue;
3079
3083
  const reason = !current ? "pair disappeared" : `spread ${currentSpread.toFixed(1)}% <= ${closeSpread}%`;
@@ -29,6 +29,18 @@ export declare function handleRates(isJson: () => boolean, opts: {
29
29
  top?: string;
30
30
  includeUnverified?: boolean;
31
31
  }): Promise<void>;
32
+ /**
33
+ * Cross-venue price basis — a PRICE comparison, independent of funding.
34
+ *
35
+ * Venue scope: every venue with the "trading" role (both legs must be executable), whether or not
36
+ * its funding rate is verified; market-data-only venues (TxFlow, unofficial API) are out. Prices
37
+ * come from each venue's market directory and are grouped by `canonical` symbol with the funding
38
+ * scan's guards (fetchCanonicalPriceGroups): unclassified / ambiguous tickers have no canonical and
39
+ * never pair, and a venue whose mark deviates more than MAX_PRICE_DEVIATION_PCT from the group
40
+ * median is dropped. The chosen cheap / rich pair must itself agree within MAX_PRICE_DEVIATION_PCT:
41
+ * a wider gap is a different asset under the same ticker (BB on Lighter 8.50 vs Aster 0.0091).
42
+ * A venue that fails is reported on stderr and is absent from the scan, never silently empty.
43
+ */
32
44
  export declare function handleBasisScan(isJson: () => boolean, opts: {
33
45
  minBasis: string;
34
46
  symbol?: string;
@@ -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;