perp-cli 0.14.3 → 0.16.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 (363) hide show
  1. package/README.ja.md +243 -0
  2. package/README.ko.md +243 -0
  3. package/README.md +138 -405
  4. package/README.zh.md +243 -0
  5. package/dist/agent-wallet/store.d.ts +3 -2
  6. package/dist/agent-wallet/store.js +2 -2
  7. package/dist/api/public/hyperliquid.js +4 -0
  8. package/dist/api/public/lighter.d.ts +5 -0
  9. package/dist/api/public/lighter.js +5 -0
  10. package/dist/api/public/pacifica.js +85 -15
  11. package/dist/api/public/urls.d.ts +2 -0
  12. package/dist/api/public/urls.js +3 -0
  13. package/dist/arb/sizing.js +4 -2
  14. package/dist/arb/state.d.ts +6 -0
  15. package/dist/arb/utils.d.ts +1 -2
  16. package/dist/arb/utils.js +1 -2
  17. package/dist/balance-by-address.d.ts +39 -0
  18. package/dist/balance-by-address.js +44 -0
  19. package/dist/bot/apex/pulse.js +3 -0
  20. package/dist/bot/apex/radar.js +2 -1
  21. package/dist/bot/engine.d.ts +10 -1
  22. package/dist/bot/engine.js +14 -6
  23. package/dist/bot/indicators.d.ts +7 -0
  24. package/dist/bot/indicators.js +16 -0
  25. package/dist/bot/perp-order.d.ts +4 -4
  26. package/dist/bot/perp-order.js +5 -5
  27. package/dist/bot/quarantine.d.ts +1 -1
  28. package/dist/bot/quarantine.js +1 -1
  29. package/dist/bot/quote-rules.js +1 -1
  30. package/dist/bot/strategies/apex-strategy.js +3 -2
  31. package/dist/bot/strategies/engine-mm.js +6 -5
  32. package/dist/bot/strategies/funding-arb-strategy.js +12 -5
  33. package/dist/bot/strategies/funding-arb-utils.d.ts +15 -3
  34. package/dist/bot/strategies/funding-arb-utils.js +53 -20
  35. package/dist/bot/strategies/funding-auto.js +2 -2
  36. package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
  37. package/dist/bot/strategies/liquidation-mm.js +8 -2
  38. package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
  39. package/dist/bot/strategies/momentum-breakout.js +11 -3
  40. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
  41. package/dist/bot/strategies/spot-perp-arb-strategy.js +6 -4
  42. package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
  43. package/dist/bot/strategies/spot-perp-executor.js +3 -3
  44. package/dist/bot/strategy-types.d.ts +2 -1
  45. package/dist/bot/tui/BotDashboard.d.ts +1 -1
  46. package/dist/bot/tui/BotDashboard.js +1 -1
  47. package/dist/cache.js +12 -6
  48. package/dist/cli-program.d.ts +15 -0
  49. package/dist/cli-program.js +126 -6
  50. package/dist/commands/account.js +168 -24
  51. package/dist/commands/agent-import.d.ts +7 -0
  52. package/dist/commands/agent-import.js +30 -1
  53. package/dist/commands/agent.d.ts +6 -3
  54. package/dist/commands/agent.js +160 -12
  55. package/dist/commands/arb/index.js +23 -81
  56. package/dist/commands/arb-auto.d.ts +24 -0
  57. package/dist/commands/arb-auto.js +175 -97
  58. package/dist/commands/arb.d.ts +20 -0
  59. package/dist/commands/arb.js +107 -163
  60. package/dist/commands/backtest.js +23 -21
  61. package/dist/commands/bot.js +1 -1
  62. package/dist/commands/builder.js +1 -1
  63. package/dist/commands/funds.js +113 -3
  64. package/dist/commands/init.d.ts +1 -1
  65. package/dist/commands/init.js +27 -7
  66. package/dist/commands/manage.js +23 -21
  67. package/dist/commands/market.d.ts +48 -0
  68. package/dist/commands/market.js +395 -100
  69. package/dist/commands/options.d.ts +14 -0
  70. package/dist/commands/options.js +151 -0
  71. package/dist/commands/pacifica-earn.d.ts +5 -0
  72. package/dist/commands/pacifica-earn.js +455 -0
  73. package/dist/commands/pacifica-orders.d.ts +14 -0
  74. package/dist/commands/pacifica-orders.js +142 -0
  75. package/dist/commands/portfolio.d.ts +16 -0
  76. package/dist/commands/portfolio.js +83 -55
  77. package/dist/commands/rebalance.d.ts +1 -0
  78. package/dist/commands/rebalance.js +15 -0
  79. package/dist/commands/risk.d.ts +11 -2
  80. package/dist/commands/risk.js +39 -44
  81. package/dist/commands/trade.d.ts +1 -0
  82. package/dist/commands/trade.js +67 -62
  83. package/dist/commands/wallet.js +1 -1
  84. package/dist/config.d.ts +2 -2
  85. package/dist/config.js +15 -4
  86. package/dist/constants.js +6 -0
  87. package/dist/cross-chain-margin.d.ts +3 -1
  88. package/dist/cross-chain-margin.js +4 -2
  89. package/dist/dashboard/server.js +3 -3
  90. package/dist/dashboard/ui.js +9 -6
  91. package/dist/errors.d.ts +29 -1
  92. package/dist/errors.js +35 -3
  93. package/dist/exchanges/address.d.ts +8 -0
  94. package/dist/exchanges/address.js +47 -0
  95. package/dist/exchanges/arcus/adapter.d.ts +16 -12
  96. package/dist/exchanges/arcus/adapter.js +45 -18
  97. package/dist/exchanges/arcus/agent.d.ts +1 -1
  98. package/dist/exchanges/arcus/agent.js +1 -1
  99. package/dist/exchanges/arcus/client.js +1 -1
  100. package/dist/exchanges/arcus/public.js +3 -2
  101. package/dist/exchanges/arcus/signing.d.ts +2 -3
  102. package/dist/exchanges/arcus/signing.js +1 -1
  103. package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
  104. package/dist/exchanges/arcus/spot-chain.js +1 -1
  105. package/dist/exchanges/arcus/spot-router.d.ts +1 -1
  106. package/dist/exchanges/arcus/spot-router.js +2 -2
  107. package/dist/exchanges/arcus/spot.d.ts +2 -3
  108. package/dist/exchanges/arcus/spot.js +2 -3
  109. package/dist/exchanges/aster-spot.d.ts +3 -3
  110. package/dist/exchanges/aster-spot.js +6 -9
  111. package/dist/exchanges/aster-typed-data.d.ts +1 -1
  112. package/dist/exchanges/aster-typed-data.js +1 -1
  113. package/dist/exchanges/aster.d.ts +30 -10
  114. package/dist/exchanges/aster.js +153 -53
  115. package/dist/exchanges/builder-fee.d.ts +1 -1
  116. package/dist/exchanges/builder-fee.js +13 -2
  117. package/dist/exchanges/canonical-symbols.d.ts +8 -2
  118. package/dist/exchanges/canonical-symbols.js +46 -2
  119. package/dist/exchanges/capabilities.d.ts +2 -0
  120. package/dist/exchanges/capabilities.js +2 -1
  121. package/dist/exchanges/derive/client.d.ts +37 -0
  122. package/dist/exchanges/derive/client.js +135 -0
  123. package/dist/exchanges/derive/options.d.ts +73 -0
  124. package/dist/exchanges/derive/options.js +379 -0
  125. package/dist/exchanges/extended/adapter.d.ts +96 -0
  126. package/dist/exchanges/extended/adapter.js +241 -0
  127. package/dist/exchanges/extended/client.d.ts +38 -0
  128. package/dist/exchanges/extended/client.js +88 -0
  129. package/dist/exchanges/extended/public.d.ts +133 -0
  130. package/dist/exchanges/extended/public.js +436 -0
  131. package/dist/exchanges/grvt/adapter.d.ts +100 -0
  132. package/dist/exchanges/grvt/adapter.js +283 -0
  133. package/dist/exchanges/grvt/client.d.ts +25 -0
  134. package/dist/exchanges/grvt/client.js +86 -0
  135. package/dist/exchanges/grvt/public.d.ts +151 -0
  136. package/dist/exchanges/grvt/public.js +438 -0
  137. package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
  138. package/dist/exchanges/hyperliquid-outcome.js +29 -1
  139. package/dist/exchanges/hyperliquid-spot.d.ts +9 -1
  140. package/dist/exchanges/hyperliquid-spot.js +24 -16
  141. package/dist/exchanges/hyperliquid.d.ts +199 -22
  142. package/dist/exchanges/hyperliquid.js +550 -202
  143. package/dist/exchanges/index.d.ts +1 -1
  144. package/dist/exchanges/index.js +1 -1
  145. package/dist/exchanges/interface.d.ts +157 -13
  146. package/dist/exchanges/interface.js +47 -0
  147. package/dist/exchanges/ioc-outcome.d.ts +1 -1
  148. package/dist/exchanges/ioc-outcome.js +1 -1
  149. package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
  150. package/dist/exchanges/legacy-trading-v2.js +7 -2
  151. package/dist/exchanges/lighter-public.d.ts +67 -12
  152. package/dist/exchanges/lighter-public.js +250 -82
  153. package/dist/exchanges/lighter-signer.d.ts +1 -1
  154. package/dist/exchanges/lighter-signer.js +1 -1
  155. package/dist/exchanges/lighter-spot.d.ts +10 -5
  156. package/dist/exchanges/lighter-spot.js +105 -97
  157. package/dist/exchanges/lighter.d.ts +80 -21
  158. package/dist/exchanges/lighter.js +312 -105
  159. package/dist/exchanges/ondo/adapter.d.ts +38 -13
  160. package/dist/exchanges/ondo/adapter.js +86 -27
  161. package/dist/exchanges/ondo/agent.d.ts +1 -1
  162. package/dist/exchanges/ondo/agent.js +1 -1
  163. package/dist/exchanges/ondo/client.d.ts +1 -1
  164. package/dist/exchanges/ondo/client.js +2 -2
  165. package/dist/exchanges/ondo/public.d.ts +35 -3
  166. package/dist/exchanges/ondo/public.js +75 -32
  167. package/dist/exchanges/ondo/spot.d.ts +2 -2
  168. package/dist/exchanges/ondo/spot.js +3 -3
  169. package/dist/exchanges/options-interface.d.ts +158 -0
  170. package/dist/exchanges/options-interface.js +17 -0
  171. package/dist/exchanges/options-venues.d.ts +12 -0
  172. package/dist/exchanges/options-venues.js +35 -0
  173. package/dist/exchanges/outcome-interface.d.ts +4 -0
  174. package/dist/exchanges/pacifica-atomic.d.ts +211 -0
  175. package/dist/exchanges/pacifica-atomic.js +440 -0
  176. package/dist/exchanges/pacifica-decimal.d.ts +10 -0
  177. package/dist/exchanges/pacifica-decimal.js +51 -0
  178. package/dist/exchanges/pacifica-presigned.d.ts +53 -0
  179. package/dist/exchanges/pacifica-presigned.js +141 -0
  180. package/dist/exchanges/pacifica-print.d.ts +200 -0
  181. package/dist/exchanges/pacifica-print.js +417 -0
  182. package/dist/exchanges/pacifica-spot.d.ts +8 -2
  183. package/dist/exchanges/pacifica-spot.js +26 -18
  184. package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
  185. package/dist/exchanges/pacifica-typed-data.js +1 -1
  186. package/dist/exchanges/pacifica-vaults.d.ts +130 -0
  187. package/dist/exchanges/pacifica-vaults.js +277 -0
  188. package/dist/exchanges/pacifica.d.ts +108 -10
  189. package/dist/exchanges/pacifica.js +428 -171
  190. package/dist/exchanges/precision.d.ts +3 -1
  191. package/dist/exchanges/precision.js +9 -2
  192. package/dist/exchanges/public-balances.d.ts +25 -0
  193. package/dist/exchanges/public-balances.js +158 -0
  194. package/dist/exchanges/public-positions.d.ts +43 -0
  195. package/dist/exchanges/public-positions.js +154 -0
  196. package/dist/exchanges/qfex/adapter.d.ts +69 -13
  197. package/dist/exchanges/qfex/adapter.js +102 -17
  198. package/dist/exchanges/qfex/agent.d.ts +1 -1
  199. package/dist/exchanges/qfex/agent.js +1 -1
  200. package/dist/exchanges/qfex/client.d.ts +1 -1
  201. package/dist/exchanges/qfex/client.js +1 -1
  202. package/dist/exchanges/qfex/errors.d.ts +1 -1
  203. package/dist/exchanges/qfex/errors.js +2 -2
  204. package/dist/exchanges/qfex/public.d.ts +65 -12
  205. package/dist/exchanges/qfex/public.js +131 -25
  206. package/dist/exchanges/qfex/signing.d.ts +2 -2
  207. package/dist/exchanges/qfex/signing.js +4 -5
  208. package/dist/exchanges/qfex/ws-transport.d.ts +3 -3
  209. package/dist/exchanges/qfex/ws-transport.js +3 -3
  210. package/dist/exchanges/rate-limiter.d.ts +1 -1
  211. package/dist/exchanges/rate-limiter.js +5 -2
  212. package/dist/exchanges/registry.d.ts +1 -1
  213. package/dist/exchanges/registry.js +1 -1
  214. package/dist/exchanges/risex/adapter.d.ts +160 -0
  215. package/dist/exchanges/risex/adapter.js +842 -0
  216. package/dist/exchanges/risex/agent.d.ts +154 -0
  217. package/dist/exchanges/risex/agent.js +384 -0
  218. package/dist/exchanges/risex/client.d.ts +47 -0
  219. package/dist/exchanges/risex/client.js +174 -0
  220. package/dist/exchanges/risex/nonce.d.ts +43 -0
  221. package/dist/exchanges/risex/nonce.js +212 -0
  222. package/dist/exchanges/risex/public.d.ts +64 -0
  223. package/dist/exchanges/risex/public.js +150 -0
  224. package/dist/exchanges/risex/session.d.ts +71 -0
  225. package/dist/exchanges/risex/session.js +171 -0
  226. package/dist/exchanges/risex/typed-data.d.ts +173 -0
  227. package/dist/exchanges/risex/typed-data.js +240 -0
  228. package/dist/exchanges/risex/units.d.ts +29 -0
  229. package/dist/exchanges/risex/units.js +104 -0
  230. package/dist/exchanges/slippage.d.ts +1 -1
  231. package/dist/exchanges/slippage.js +1 -1
  232. package/dist/exchanges/spot-factory.d.ts +3 -3
  233. package/dist/exchanges/spot-factory.js +8 -8
  234. package/dist/exchanges/spot-interface.d.ts +5 -5
  235. package/dist/exchanges/spot-interface.js +2 -2
  236. package/dist/exchanges/spot-result.d.ts +3 -3
  237. package/dist/exchanges/spot-result.js +3 -3
  238. package/dist/exchanges/trading-v2.d.ts +3 -3
  239. package/dist/exchanges/trading-v2.js +9 -6
  240. package/dist/exchanges/txflow/adapter.d.ts +61 -0
  241. package/dist/exchanges/txflow/adapter.js +150 -0
  242. package/dist/exchanges/txflow/client.d.ts +47 -0
  243. package/dist/exchanges/txflow/client.js +155 -0
  244. package/dist/exchanges/txflow/public.d.ts +87 -0
  245. package/dist/exchanges/txflow/public.js +355 -0
  246. package/dist/exchanges/unknown-symbol.d.ts +34 -0
  247. package/dist/exchanges/unknown-symbol.js +56 -0
  248. package/dist/exchanges/variational/adapter.d.ts +67 -0
  249. package/dist/exchanges/variational/adapter.js +137 -0
  250. package/dist/exchanges/variational/client.d.ts +21 -0
  251. package/dist/exchanges/variational/client.js +66 -0
  252. package/dist/exchanges/variational/public.d.ts +126 -0
  253. package/dist/exchanges/variational/public.js +430 -0
  254. package/dist/exchanges/venue-types.d.ts +52 -10
  255. package/dist/exchanges/venue-types.js +82 -11
  256. package/dist/exchanges/venue.d.ts +150 -11
  257. package/dist/exchanges/venue.js +126 -4
  258. package/dist/exchanges/venues/arcus.d.ts +1 -2
  259. package/dist/exchanges/venues/arcus.js +5 -4
  260. package/dist/exchanges/venues/aster.d.ts +2 -2
  261. package/dist/exchanges/venues/aster.js +6 -4
  262. package/dist/exchanges/venues/create-helpers.d.ts +1 -1
  263. package/dist/exchanges/venues/create-helpers.js +3 -3
  264. package/dist/exchanges/venues/extended.d.ts +21 -0
  265. package/dist/exchanges/venues/extended.js +96 -0
  266. package/dist/exchanges/venues/grvt.d.ts +30 -0
  267. package/dist/exchanges/venues/grvt.js +98 -0
  268. package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
  269. package/dist/exchanges/venues/hyperliquid.js +19 -8
  270. package/dist/exchanges/venues/index.d.ts +7 -2
  271. package/dist/exchanges/venues/index.js +8 -2
  272. package/dist/exchanges/venues/lighter-family.d.ts +7 -9
  273. package/dist/exchanges/venues/lighter-family.js +9 -11
  274. package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
  275. package/dist/exchanges/venues/lighter-rh.js +20 -8
  276. package/dist/exchanges/venues/lighter.d.ts +1 -1
  277. package/dist/exchanges/venues/lighter.js +18 -6
  278. package/dist/exchanges/venues/ondo.d.ts +1 -1
  279. package/dist/exchanges/venues/ondo.js +5 -3
  280. package/dist/exchanges/venues/pacifica.d.ts +2 -2
  281. package/dist/exchanges/venues/pacifica.js +15 -4
  282. package/dist/exchanges/venues/qfex.d.ts +1 -1
  283. package/dist/exchanges/venues/qfex.js +4 -2
  284. package/dist/exchanges/venues/risex-release.d.ts +14 -0
  285. package/dist/exchanges/venues/risex-release.js +14 -0
  286. package/dist/exchanges/venues/risex.d.ts +31 -0
  287. package/dist/exchanges/venues/risex.js +131 -0
  288. package/dist/exchanges/venues/txflow.d.ts +18 -0
  289. package/dist/exchanges/venues/txflow.js +77 -0
  290. package/dist/exchanges/venues/variational.d.ts +21 -0
  291. package/dist/exchanges/venues/variational.js +85 -0
  292. package/dist/execution-log.d.ts +1 -1
  293. package/dist/funding/legacy-public.d.ts +6 -4
  294. package/dist/funding/legacy-public.js +42 -41
  295. package/dist/funding/normalize.d.ts +1 -1
  296. package/dist/funding/normalize.js +1 -1
  297. package/dist/funding/rates.d.ts +95 -6
  298. package/dist/funding/rates.js +99 -19
  299. package/dist/index.js +7 -6
  300. package/dist/jobs.d.ts +1 -0
  301. package/dist/jobs.js +25 -1
  302. package/dist/landing.d.ts +2 -0
  303. package/dist/landing.js +1 -1
  304. package/dist/liquidity.d.ts +3 -1
  305. package/dist/liquidity.js +8 -4
  306. package/dist/mcp-pacifica-earn.d.ts +9 -0
  307. package/dist/mcp-pacifica-earn.js +64 -0
  308. package/dist/mcp-server.js +211 -68
  309. package/dist/pacifica/client.d.ts +69 -5
  310. package/dist/pacifica/client.js +181 -8
  311. package/dist/pacifica/rate-limit.d.ts +144 -0
  312. package/dist/pacifica/rate-limit.js +267 -0
  313. package/dist/pacifica/types/order.d.ts +2 -1
  314. package/dist/plan-executor.js +7 -2
  315. package/dist/positions-by-address.d.ts +34 -0
  316. package/dist/positions-by-address.js +46 -0
  317. package/dist/rebalance.js +1 -1
  318. package/dist/risk.d.ts +2 -1
  319. package/dist/risk.js +2 -4
  320. package/dist/settings.d.ts +32 -3
  321. package/dist/signer/ows-evm.d.ts +1 -1
  322. package/dist/signer/ows-evm.js +1 -1
  323. package/dist/smart-order.d.ts +1 -1
  324. package/dist/smart-order.js +1 -1
  325. package/dist/strategies/funding-arb.d.ts +1 -1
  326. package/dist/strategies/funding-arb.js +36 -63
  327. package/dist/trade-validator.js +2 -2
  328. package/dist/utils.d.ts +22 -3
  329. package/dist/utils.js +11 -3
  330. package/dist/venue-health.d.ts +40 -0
  331. package/dist/venue-health.js +75 -0
  332. package/dist/withdraw-gate.d.ts +1 -1
  333. package/dist/withdraw-gate.js +1 -1
  334. package/dist/ws/arcus-market.d.ts +9 -0
  335. package/dist/ws/arcus-market.js +9 -0
  336. package/dist/ws/aster-market.d.ts +33 -0
  337. package/dist/ws/aster-market.js +30 -0
  338. package/dist/ws/index.d.ts +980 -0
  339. package/dist/ws/index.js +4460 -0
  340. package/dist/ws/lighter-market.d.ts +14 -0
  341. package/dist/ws/lighter-market.js +13 -0
  342. package/dist/ws/node.d.ts +996 -0
  343. package/dist/ws/node.js +4554 -0
  344. package/dist/ws/pacifica-instrument.d.ts +8 -0
  345. package/dist/ws/pacifica-instrument.js +8 -0
  346. package/dist/ws/unclassified.d.ts +16 -0
  347. package/dist/ws/unclassified.js +43 -0
  348. package/dist/ws-cli/credentials.d.ts +62 -0
  349. package/dist/ws-cli/credentials.js +126 -0
  350. package/package.json +71 -10
  351. package/skills/perp-cli/SKILL.md +153 -36
  352. package/skills/perp-cli/package.json +15 -2
  353. package/skills/perp-cli/references/agent-operations.md +22 -12
  354. package/skills/perp-cli/references/commands.md +143 -17
  355. package/skills/perp-cli/references/strategies.md +38 -3
  356. package/skills/perp-cli/references/ws-library.md +523 -0
  357. package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
  358. package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
  359. package/skills/perp-cli/scripts/preflight.sh +27 -19
  360. package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
  361. package/skills/perp-cli/scripts/validate-arb.sh +56 -39
  362. package/dist/cli-spec.d.ts +0 -46
  363. package/dist/cli-spec.js +0 -81
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Spot-Perp Arbitrage Strategy
3
3
  *
4
- * Focused strategy: buy spot (HL or Lighter) + short perp on the BEST of 4 exchanges.
4
+ * Focused strategy: buy spot (HL or Lighter) + short perp on the best of the configured perp exchanges (`exchanges`).
5
5
  * No perp-perp logic. History-based entry/exit with short rotation.
6
6
  *
7
7
  * Strategy layer handles ONLY decisions (opportunity scanning, scoring, close/rotation decisions).
@@ -12,7 +12,7 @@ import { registerStrategy } from "../strategy-registry.js";
12
12
  import { getFundingHours } from "../../funding.js";
13
13
  import { annualizeHourlyRate } from "../../funding/normalize.js";
14
14
  import { pruneExecutionLog } from "../../execution-log.js";
15
- import { buildAdapterMap, getPerpSymbol, findRate, cappedLeverage, matchSymbol, fetchRates, scoreFunding, botSpotPerpMatches, assertFundingArbLegs, cancelBotSymbols, collectFundingSince } from "./funding-arb-utils.js";
15
+ import { buildAdapterMap, getPerpSymbol, findRate, cappedLeverage, matchSymbol, fetchRates, scoreFunding, assertFundingHistoryPeriods, botSpotPerpMatches, assertFundingArbLegs, cancelBotSymbols, collectFundingSince } from "./funding-arb-utils.js";
16
16
  import { SpotPerpExecutor } from "./spot-perp-executor.js";
17
17
  import { findSpotQuoteBalance, getBotSpotAdapter, nonQuoteSpotBalances, spotNetworkOf, spotSharesPerpMargin, spotTokenBase } from "../../exchanges/spot-factory.js";
18
18
  import { getVenue } from "../../exchanges/venue.js";
@@ -80,6 +80,8 @@ export class SpotPerpArbStrategy {
80
80
  catch { /* non-critical */ }
81
81
  this._config = ctx.config;
82
82
  const p = this.params;
83
+ // Config-time check: a history_periods above a leg venue's cap would fail every tick.
84
+ assertFundingHistoryPeriods(p.history_periods, p.exchanges);
83
85
  ctx.state.set("arbRunning", true);
84
86
  ctx.state.set("spaPositions", []);
85
87
  ctx.state.set("arbPositions", 0);
@@ -285,7 +287,7 @@ export class SpotPerpArbStrategy {
285
287
  ctx.state.set("spaPositions", remaining);
286
288
  ctx.state.set("arbPositions", remaining.length);
287
289
  }
288
- // ── Rotation check (only to perp venues that match the spot leg, §12.4 #6) ──
290
+ // ── Rotation check (only to perp venues that match the spot leg) ──
289
291
  const perpMarketsCache = new Map();
290
292
  const allowedBySpotVenue = new Map();
291
293
  const currentPositions = ctx.state.get("spaPositions");
@@ -375,7 +377,7 @@ export class SpotPerpArbStrategy {
375
377
  const spotAdapter = await getBotSpotAdapter(spotExName, spotExAdapter);
376
378
  if (!spotAdapter)
377
379
  continue;
378
- // Canonical symbol + price guard (§12.4 #6): base → perp venues it may hedge on.
380
+ // Canonical symbol + price guard: base → perp venues it may hedge on.
379
381
  const allowedByBase = await this.spotPerpVenues(spotExName, await spotAdapter.getSpotMarkets(), adapters, perpMarketsCache, ctx);
380
382
  for (const [base, allowed] of allowedByBase) {
381
383
  // HL spot: skip tokens not in whitelist
@@ -26,7 +26,7 @@ export type ExecutionResult = {
26
26
  /**
27
27
  * closed = position fully closed; remainingSize = partially closed, this size is still open (hedged).
28
28
  * manual = the outcome is unknown / unverified or a leg is unhedged: the caller must quarantine
29
- * the position and never retry it automatically (design §12.3).
29
+ * the position and never retry it automatically.
30
30
  */
31
31
  export type CloseResult = {
32
32
  closed: true;
@@ -65,8 +65,8 @@ export class SpotPerpExecutor {
65
65
  }
66
66
  // Price estimate
67
67
  const price = await getPriceEstimate(perpAdapter, perpSymbol, symbol);
68
- if (price <= 0)
69
- return { success: false, error: "price <= 0" };
68
+ if (price === null)
69
+ return { success: false, error: `no positive mark price for ${perpSymbol} on ${perpExchange}` };
70
70
  // Liquidity check
71
71
  const liq = await checkSpotPerpLiquidity(spotAdapter, perpAdapter, symbol, perpSymbol, sizeUsd, 0.5, (msg) => log(` ${msg}`));
72
72
  if (!liq.viable)
@@ -140,7 +140,7 @@ export class SpotPerpExecutor {
140
140
  await this.returnToPerp(spotAdapter, actualTransferred, spotExchange, log);
141
141
  return { success: false, error: `pre-trade balance: ${msg}` };
142
142
  }
143
- // Step 1: Buy spot — the hedge is sized to what actually filled (§12.4 #5)
143
+ // Step 1: Buy spot — the hedge is sized to what actually filled
144
144
  let fill;
145
145
  try {
146
146
  fill = await spotMarketFill(spotAdapter, symbol, "buy", matched.size, spotStep);
@@ -10,7 +10,8 @@ export interface EnrichedSnapshot extends MarketSnapshot {
10
10
  bids: [string, string][];
11
11
  asks: [string, string][];
12
12
  };
13
- openInterest: string;
13
+ /** Base-asset open interest; null = not read / not published (never "0" as a stand-in). */
14
+ openInterest: string | null;
14
15
  }
15
16
  export type StrategyAction = {
16
17
  type: "place_order";
@@ -28,7 +28,7 @@ export interface BotTuiState {
28
28
  exchange: string;
29
29
  price: number;
30
30
  volume24h: number;
31
- openInterest: string;
31
+ openInterest: string | null;
32
32
  fundingRate: number;
33
33
  volatility24h: number;
34
34
  positions: Position[];
@@ -78,7 +78,7 @@ export function BotDashboard({ initialState, onQuit, onPause, subscribe }) {
78
78
  const askOrders = state.openOrders.filter((o) => o.side.toLowerCase() === "sell");
79
79
  const hasMultiExchange = (state.exchangeBalances?.length ?? 0) > 1;
80
80
  const isMultiSymbol = state.symbol.toUpperCase() === "ALL";
81
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.strategy, " | ", state.symbol, " | ", state.exchange] }), _jsxs(Text, { color: "gray", children: ["Runtime: ", formatDuration(state.runtime), " | Tick: ", state.tick] })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: phaseColor(state.phase), children: phaseIndicator(state.phase) }) })] }), isMultiSymbol ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Exchanges: ", _jsx(Text, { bold: true, color: "white", children: state.exchangeBalances?.length ?? 1 })] }), _jsxs(Text, { color: "gray", children: ["Positions: ", _jsx(Text, { bold: true, color: state.positions.length > 0 ? "yellow" : "white", children: state.positions.length })] }), _jsxs(Text, { color: "gray", children: ["Total Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(totalEquity) })] }), _jsxs(Text, { color: "gray", children: ["PnL: ", _jsxs(Text, { bold: true, color: state.totalPnl >= 0 ? "green" : "red", children: ["$", state.totalPnl.toFixed(2)] })] })] })) : (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Price: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.price.toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["Vol24h: ", _jsx(Text, { color: "white", children: Number.isFinite(state.volatility24h) ? `${state.volatility24h.toFixed(1)}%` : "unknown" })] }), _jsxs(Text, { color: "gray", children: ["FR: ", _jsxs(Text, { color: state.fundingRate >= 0 ? "green" : "red", children: [(state.fundingRate * 100).toFixed(4), "%"] })] }), _jsxs(Text, { color: "gray", children: ["OI: ", _jsx(Text, { color: "white", children: state.openInterest })] })] })), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Account" }), _jsxs(Text, { children: ["Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(state.equity) }), _jsxs(Text, { color: "gray", children: [" (peak: ", usd(state.peakEquity), ")"] })] }), _jsxs(Text, { children: ["Daily P&L: ", _jsxs(Text, { bold: true, color: pnlColor(state.dailyPnl), children: ["$", state.dailyPnl.toFixed(2)] })] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: ["Drawdown: ", _jsx(Text, { bold: true, color: drawdown === null ? "gray" : drawdown > 0 ? "red" : "green", children: drawdown === null ? "unknown" : `${usd(drawdown)} (${drawdownPct.toFixed(1)}%)` })] }), _jsxs(Text, { children: ["Orders: ", _jsx(Text, { bold: true, color: "white", children: state.ordersPlaced }), _jsx(Text, { color: "gray", children: " | Fills: " }), _jsx(Text, { bold: true, color: "white", children: state.fills }), _jsx(Text, { color: "gray", children: " | Total P&L: " }), _jsxs(Text, { bold: true, color: pnlColor(state.totalPnl), children: ["$", state.totalPnl.toFixed(2)] })] })] })] }), state.exchangeBalances && state.exchangeBalances.length > 1 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Exchange Balances" }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: pad("Exchange", 16) }), _jsx(Text, { color: "gray", children: pad("Equity", 14) }), _jsx(Text, { color: "gray", children: "Available" })] }), state.exchangeBalances.map((eb, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "white", children: pad(eb.exchange, 16) }), eb.equity === null ? (_jsxs(Text, { color: "red", children: ["unavailable", eb.error ? ` (${eb.error})` : ""] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "white", children: pad(`$${parseFloat(eb.equity).toFixed(2)}`, 14) }), _jsx(Text, { color: "gray", children: eb.available === null ? "unavailable" : `$${parseFloat(eb.available).toFixed(2)}` })] }))] }, i)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Positions" }), positionErrors.map((pe, i) => (_jsxs(Text, { color: "red", children: [" ", pe.exchange, ": unavailable (", pe.error, ")"] }, `err-${i}`))), state.positions.length === 0 ? (positionErrors.length === 0 && _jsx(Text, { color: "gray", children: " No open positions" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "gray", children: pad("Exchange", 14) }), _jsx(Text, { color: "gray", children: pad("Symbol", 10) }), _jsx(Text, { color: "gray", children: pad("Side", 6) }), _jsx(Text, { color: "gray", children: pad("Size", 12) }), _jsx(Text, { color: "gray", children: pad("Entry", 12) }), _jsx(Text, { color: "gray", children: pad("Mark", 12) }), _jsx(Text, { color: "gray", children: "uPnL" })] }), state.positions.map((p, i) => {
81
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.strategy, " | ", state.symbol, " | ", state.exchange] }), _jsxs(Text, { color: "gray", children: ["Runtime: ", formatDuration(state.runtime), " | Tick: ", state.tick] })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: phaseColor(state.phase), children: phaseIndicator(state.phase) }) })] }), isMultiSymbol ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Exchanges: ", _jsx(Text, { bold: true, color: "white", children: state.exchangeBalances?.length ?? 1 })] }), _jsxs(Text, { color: "gray", children: ["Positions: ", _jsx(Text, { bold: true, color: state.positions.length > 0 ? "yellow" : "white", children: state.positions.length })] }), _jsxs(Text, { color: "gray", children: ["Total Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(totalEquity) })] }), _jsxs(Text, { color: "gray", children: ["PnL: ", _jsxs(Text, { bold: true, color: state.totalPnl >= 0 ? "green" : "red", children: ["$", state.totalPnl.toFixed(2)] })] })] })) : (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Price: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.price.toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["Vol24h: ", _jsx(Text, { color: "white", children: Number.isFinite(state.volatility24h) ? `${state.volatility24h.toFixed(1)}%` : "unknown" })] }), _jsxs(Text, { color: "gray", children: ["FR: ", _jsxs(Text, { color: state.fundingRate >= 0 ? "green" : "red", children: [(state.fundingRate * 100).toFixed(4), "%"] })] }), _jsxs(Text, { color: "gray", children: ["OI: ", _jsx(Text, { color: "white", children: state.openInterest ?? "-" })] })] })), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Account" }), _jsxs(Text, { children: ["Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(state.equity) }), _jsxs(Text, { color: "gray", children: [" (peak: ", usd(state.peakEquity), ")"] })] }), _jsxs(Text, { children: ["Daily P&L: ", _jsxs(Text, { bold: true, color: pnlColor(state.dailyPnl), children: ["$", state.dailyPnl.toFixed(2)] })] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: ["Drawdown: ", _jsx(Text, { bold: true, color: drawdown === null ? "gray" : drawdown > 0 ? "red" : "green", children: drawdown === null ? "unknown" : `${usd(drawdown)} (${drawdownPct.toFixed(1)}%)` })] }), _jsxs(Text, { children: ["Orders: ", _jsx(Text, { bold: true, color: "white", children: state.ordersPlaced }), _jsx(Text, { color: "gray", children: " | Fills: " }), _jsx(Text, { bold: true, color: "white", children: state.fills }), _jsx(Text, { color: "gray", children: " | Total P&L: " }), _jsxs(Text, { bold: true, color: pnlColor(state.totalPnl), children: ["$", state.totalPnl.toFixed(2)] })] })] })] }), state.exchangeBalances && state.exchangeBalances.length > 1 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Exchange Balances" }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: pad("Exchange", 16) }), _jsx(Text, { color: "gray", children: pad("Equity", 14) }), _jsx(Text, { color: "gray", children: "Available" })] }), state.exchangeBalances.map((eb, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "white", children: pad(eb.exchange, 16) }), eb.equity === null ? (_jsxs(Text, { color: "red", children: ["unavailable", eb.error ? ` (${eb.error})` : ""] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "white", children: pad(`$${parseFloat(eb.equity).toFixed(2)}`, 14) }), _jsx(Text, { color: "gray", children: eb.available === null ? "unavailable" : `$${parseFloat(eb.available).toFixed(2)}` })] }))] }, i)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Positions" }), positionErrors.map((pe, i) => (_jsxs(Text, { color: "red", children: [" ", pe.exchange, ": unavailable (", pe.error, ")"] }, `err-${i}`))), state.positions.length === 0 ? (positionErrors.length === 0 && _jsx(Text, { color: "gray", children: " No open positions" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "gray", children: pad("Exchange", 14) }), _jsx(Text, { color: "gray", children: pad("Symbol", 10) }), _jsx(Text, { color: "gray", children: pad("Side", 6) }), _jsx(Text, { color: "gray", children: pad("Size", 12) }), _jsx(Text, { color: "gray", children: pad("Entry", 12) }), _jsx(Text, { color: "gray", children: pad("Mark", 12) }), _jsx(Text, { color: "gray", children: "uPnL" })] }), state.positions.map((p, i) => {
82
82
  const upnl = parseFloat(p.unrealizedPnl);
83
83
  return (_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "cyan", children: pad(p.exchange ?? "—", 14) }), _jsx(Text, { color: "white", children: pad(p.symbol, 10) }), _jsx(Text, { color: p.side.toLowerCase() === "long" ? "green" : "red", children: pad(p.side, 6) }), _jsx(Text, { color: "white", children: pad(p.size, 12) }), _jsx(Text, { color: "white", children: pad(p.entryPrice, 12) }), _jsx(Text, { color: "white", children: pad(p.markPrice, 12) }), _jsx(Text, { color: isNaN(upnl) ? "gray" : pnlColor(upnl), children: isNaN(upnl) ? "$—" : `$${upnl.toFixed(2)}` })] }, i));
84
84
  })] }))] }), openOrdersError !== null ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["Open orders unavailable (", openOrdersError, ")"] }) })) : (_jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "green", bold: true, children: ["Bids (", bidOrders.length, ")"] }), bidOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (bidOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "green", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] }), _jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "red", bold: true, children: ["Asks (", askOrders.length, ")"] }), askOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (askOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "red", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] })] })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: state.strategyState.fundingTotal !== undefined ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "cyan", bold: true, children: "Funding & Arb" }), _jsxs(Box, { flexDirection: "row", marginLeft: 2, children: [_jsx(Text, { color: "gray", children: "Funding Income: " }), _jsx(Text, { color: "white", children: String(state.strategyState.fundingTotal) }), _jsx(Text, { color: "gray", children: " Positions: " }), _jsx(Text, { color: "white", children: String(state.strategyState.arbPositions ?? 0) })] }), Array.isArray(state.strategyState.arbPositionDetails) &&
package/dist/cache.js CHANGED
@@ -7,6 +7,7 @@
7
7
  import { mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, unlinkSync } from "fs";
8
8
  import { join } from "path";
9
9
  import { tmpdir } from "os";
10
+ import { randomBytes } from "crypto";
10
11
  const CACHE_DIR = join(tmpdir(), "perp-cli-cache");
11
12
  // TTL presets
12
13
  export const TTL_ACCOUNT = 15_000; // 15s — balance, positions, orders (ticks are 30s; 15s ensures same-tick dedup)
@@ -41,16 +42,21 @@ export function getCached(key) {
41
42
  }
42
43
  export function setCached(key, data, ttlMs) {
43
44
  ensureDir();
45
+ const file = join(CACHE_DIR, `${safeKey(key)}.json`);
46
+ // Unique per writer: concurrent processes writing the same key must not share one tmp file
47
+ // (a rename of another writer's half-written tmp would publish a partial entry).
48
+ const tmp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
44
49
  try {
45
50
  const entry = { ts: Date.now(), ttl: ttlMs, data };
46
- const content = JSON.stringify(entry);
47
- const file = join(CACHE_DIR, `${safeKey(key)}.json`);
48
- const tmp = file + ".tmp";
49
- writeFileSync(tmp, content);
51
+ writeFileSync(tmp, JSON.stringify(entry));
50
52
  renameSync(tmp, file);
51
53
  }
52
54
  catch {
53
- // cache write failure is non-fatal
55
+ // cache write failure is non-fatal; never leave this writer's tmp file behind
56
+ try {
57
+ unlinkSync(tmp);
58
+ }
59
+ catch { /* not written */ }
54
60
  }
55
61
  }
56
62
  /**
@@ -94,7 +100,7 @@ export function invalidateCache(keyPrefix) {
94
100
  const files = readdirSync(CACHE_DIR);
95
101
  for (const f of files) {
96
102
  if (!f.endsWith(".json"))
97
- continue;
103
+ continue; // also skips in-flight "<key>.json.<pid>.<rand>.tmp" files
98
104
  if (!prefix || f.startsWith(prefix)) {
99
105
  try {
100
106
  unlinkSync(join(CACHE_DIR, f));
@@ -28,8 +28,23 @@ export declare function landingData(program: Command, version: string): {
28
28
  name: string;
29
29
  description: string;
30
30
  }[];
31
+ venues: {
32
+ id: string;
33
+ displayName: string;
34
+ roles: string[];
35
+ apiStatus: "official" | "unofficial";
36
+ note: string | null;
37
+ }[];
31
38
  };
32
39
  export declare function buildProgram(opts: {
33
40
  defaultExchange: string;
34
41
  version: string;
35
42
  }): CliProgram;
43
+ /**
44
+ * The trading-role gate at command resolution: `perp -e <venue> trade <write>`, every
45
+ * `perp trade multi <venue>:…` leg and `perp wallet agent approve|import|revoke|rotate <venue>`
46
+ * (revoke --force / --local-only excepted: local cleanup only) and every `strategy … --exchanges` leg refuse with roleUnsupported() (CAPABILITY_UNSUPPORTED) when the venue has no trading role. Runs
47
+ * before the dry-run guard and before any action, so a dry-run plan can never present a gated
48
+ * venue as tradable. Unknown venue names are left to the command's own UNKNOWN_VENUE handling.
49
+ */
50
+ export declare function assertCommandVenueRole(actionCommand: Command, exchangeOpt: string): void;
@@ -6,13 +6,15 @@ import { Command } from "commander";
6
6
  import { cliCredentialOptsFrom, hasCredentials, resolveCredentials } from "./config.js";
7
7
  import { PacificaAdapter } from "./exchanges/pacifica.js";
8
8
  import { HyperliquidAdapter } from "./exchanges/hyperliquid.js";
9
- import { getVenue, listVenues, resolveVenueId, READ_ONLY_CREDENTIALS } from "./exchanges/venue.js";
9
+ import { getVenue, listVenues, resolveVenueId, roleUnsupported, tryResolveVenueId, READ_ONLY_CREDENTIALS } from "./exchanges/venue.js";
10
10
  import { registerAdapterNetwork } from "./exchanges/adapter-network.js";
11
11
  import "./exchanges/venues/index.js";
12
12
  import { PerpError } from "./errors.js";
13
13
  import { registerMarketCommands } from "./commands/market.js";
14
14
  import { registerAccountCommands } from "./commands/account.js";
15
15
  import { registerOutcomeCommands } from "./commands/outcome.js";
16
+ import { registerOptionsCommands } from "./commands/options.js";
17
+ import { registerPacificaEarnCommands } from "./commands/pacifica-earn.js";
16
18
  import { registerTradeCommands } from "./commands/trade.js";
17
19
  // manage commands now register under `wallet manage ...` via registerWalletCommands.
18
20
  // stream commands removed — WS feeds still used by dashboard/event-stream internally
@@ -86,6 +88,11 @@ export function landingData(program, version) {
86
88
  return {
87
89
  name: program.name(),
88
90
  version,
91
+ // Registry facts only (no reads): an "unofficial" venue is read through an undocumented API.
92
+ venues: listVenues().map(v => ({
93
+ id: v.id, displayName: v.displayName, roles: [...v.roles], apiStatus: v.apiStatus.kind,
94
+ note: v.apiStatus.kind === "unofficial" ? v.apiStatus.note : null,
95
+ })),
89
96
  usage: `${program.name()} [options] <command> [args] (add --json for one JSON document on stdout)`,
90
97
  help: `${program.name()} <command> --help`,
91
98
  commands: program.commands
@@ -104,7 +111,7 @@ export function buildProgram(opts) {
104
111
  .option("-e, --exchange <exchange>", `Exchange: ${listVenues().map(v => v.id).join(", ")} (default: ${defaultExchange})`, defaultExchange)
105
112
  .option("-k, --private-key <key>", "REJECTED — a raw key on argv leaks via ps / shell history. Import it instead: perp wallet set <exchange> (stdin)")
106
113
  .option("--json", "Output raw JSON (for piping)")
107
- .option("--fields <fields>", "Comma-separated fields to include in JSON output (e.g. totalEquity,positions)")
114
+ .option("--fields <fields>", "Comma-separated fields to include in JSON output (dot paths allowed, e.g. totals.equity,exchanges)")
108
115
  .option("--ndjson", "Output newline-delimited JSON (one object per line for streaming)")
109
116
  .option("-w, --wallet <name>", "Use a specific wallet by name (from 'perp wallet list')")
110
117
  .option("--ows <name>", "Use an OWS (Open Wallet Standard) wallet by name")
@@ -131,9 +138,9 @@ export function buildProgram(opts) {
131
138
  });
132
139
  let _adapter = null;
133
140
  /**
134
- * One adapter per `${venueId}:${network}:${wallet}:${hlDex}` (design §3 L1 cache key).
141
+ * One adapter per `${venueId}:${network}:${wallet}:${hlDex}`.
135
142
  * Every adapter is built by the venue definition's create(ctx) — the single
136
- * creation path shared with the MCP server (design §10.2).
143
+ * creation path shared with the MCP server.
137
144
  */
138
145
  const _adapters = new Map();
139
146
  function cliCredentialOpts() {
@@ -147,7 +154,7 @@ export function buildProgram(opts) {
147
154
  const wallet = opts.ows ? `ows:${opts.ows}` : (opts.wallet ?? "default");
148
155
  return `${venue}:${network}:${wallet}:${hlDex ?? "-"}`;
149
156
  }
150
- /** restrictedRegions notice: stderr, once per venue per process (design §9.1 #2 — warn, never block). */
157
+ /** restrictedRegions notice: stderr, once per venue per process (warn, never block). */
151
158
  const _restrictedWarned = new Set();
152
159
  function warnRestrictedRegions(def) {
153
160
  if (def.restrictedRegions.length === 0 || _restrictedWarned.has(def.id))
@@ -167,6 +174,10 @@ export function buildProgram(opts) {
167
174
  remediation: `${def.id} is market-data only — use the funding/market public commands (e.g. perp arb scan)`,
168
175
  });
169
176
  }
177
+ // A market-data-only venue (TxFlow: unofficial read-only API) never gets a credentialed adapter:
178
+ // every account / trading / funds / bot path refuses here, before any credential or request.
179
+ if (!publicOnly && !def.roles.includes("account"))
180
+ throw roleUnsupported(def, "account");
170
181
  const network = currentNetwork();
171
182
  const hlDex = def.id === "hyperliquid" ? dex : null;
172
183
  const key = publicOnly ? `${def.id}:${network}:public:${hlDex ?? "-"}` : adapterCacheKey(def.id, network, hlDex);
@@ -246,6 +257,8 @@ export function buildProgram(opts) {
246
257
  registerMarketCommands(program, getPublicAdapter, isJson, getPublicAdapterForExchange, getAdapter);
247
258
  registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange);
248
259
  registerOutcomeCommands(program, getAdapterForExchange, isJson);
260
+ registerOptionsCommands(program, isJson);
261
+ registerPacificaEarnCommands(program, getAdapterForExchange, isJson);
249
262
  registerTradeCommands(program, getAdapter, isJson, isDryRunFlag, getAdapterForExchange);
250
263
  // manage tree wired inside registerWalletCommands below.
251
264
  // stream commands removed
@@ -273,7 +286,15 @@ export function buildProgram(opts) {
273
286
  // registerFundsCommands above.
274
287
  // Background processes & strategies
275
288
  registerBackgroundCommands(program, isJson, getAdapterForExchange);
276
- registerStrategyCommands(program, getAdapter, getAdapterForExchange, isJson);
289
+ // Every strategy command (bots, plans, TWAP / DCA / grid, `strategy run`, `--exchanges` legs,
290
+ // config-file venues) builds its adapters through these: a venue without the trading role
291
+ // (TxFlow, release-gated RISEx) is refused here, before any key is opened — also under --dry-run.
292
+ const strategyVenueGate = (venue) => {
293
+ const def = getVenue(venue);
294
+ if (!def.roles.includes("trading"))
295
+ throw roleUnsupported(def, "automation");
296
+ };
297
+ registerStrategyCommands(program, async () => { strategyVenueGate(getExchange()); return getAdapter(); }, async (exchange) => { strategyVenueGate(exchange); return getAdapterForExchange(exchange); }, isJson);
277
298
  // Agent-friendly commands
278
299
  registerRiskCommands(program, getAdapterForExchange, isJson);
279
300
  registerHistoryCommands(program, isJson, getAdapterForExchange);
@@ -286,7 +307,12 @@ export function buildProgram(opts) {
286
307
  registerPortfolioCommand(program, getAdapterForExchange, isJson, version, getHLAdapterForDex);
287
308
  registerInitCommand(program);
288
309
  registerAlertCommands(program, isJson);
310
+ installNestedHelp(program);
311
+ installMissingSubcommandError(program);
289
312
  // Switch shared API URLs if --network testnet is used
313
+ // A venue without the trading role (market-data-only TxFlow, release-gated RISEx) refuses every
314
+ // trading command before anything else — also under --dry-run, whose plans skip the adapter.
315
+ program.hook("preAction", (_root, actionCommand) => assertCommandVenueRole(actionCommand, program.opts().exchange));
290
316
  // Validates -n and enforces default-deny --dry-run before any action runs.
291
317
  installDryRunGuard(program);
292
318
  // Secrets on argv (review L10): -k is rejected outright; --passphrase still works but warns loudly.
@@ -301,3 +327,97 @@ export function buildProgram(opts) {
301
327
  program.hook("preAction", () => setQuarantineNetwork(networkOf(program)));
302
328
  return { program, getAdapterForExchange, isJson };
303
329
  }
330
+ /** `perp trade <leaf>` commands that only read (order status, fills, P&L tracking, spot reads). */
331
+ const TRADE_READ_COMMANDS = new Set(["status", "fills", "pnl-track", "spot-orders", "spot-balance"]);
332
+ /**
333
+ * The trading-role gate at command resolution: `perp -e <venue> trade <write>`, every
334
+ * `perp trade multi <venue>:…` leg and `perp wallet agent approve|import|revoke|rotate <venue>`
335
+ * (revoke --force / --local-only excepted: local cleanup only) and every `strategy … --exchanges` leg refuse with roleUnsupported() (CAPABILITY_UNSUPPORTED) when the venue has no trading role. Runs
336
+ * before the dry-run guard and before any action, so a dry-run plan can never present a gated
337
+ * venue as tradable. Unknown venue names are left to the command's own UNKNOWN_VENUE handling.
338
+ */
339
+ export function assertCommandVenueRole(actionCommand, exchangeOpt) {
340
+ const path = commandPath(actionCommand).split(" ");
341
+ const leaf = path[path.length - 1];
342
+ let venueArgs;
343
+ let feature;
344
+ if (leaf === "multi" && (path[0] === "trade" || path.length === 1)) {
345
+ venueArgs = actionCommand.args.map(leg => leg.split(":")[0]);
346
+ feature = "trading";
347
+ }
348
+ else if (path[0] === "strategy" && typeof actionCommand.opts().exchanges === "string") {
349
+ // `--exchanges` legs (strategy run / funding-arb / quick-arb / delta-neutral, incl. their defaults):
350
+ // refused before the first leg's adapter is built. The -e venue is gated by the strategy adapter factory.
351
+ venueArgs = String(actionCommand.opts().exchanges).split(",").map(v => v.trim()).filter(v => v !== "");
352
+ feature = "automation";
353
+ }
354
+ else if (path[0] === "trade" && path.length === 2 && !TRADE_READ_COMMANDS.has(leaf)) {
355
+ venueArgs = [exchangeOpt];
356
+ feature = "trading";
357
+ }
358
+ else if (path[0] === "wallet" && path[1] === "agent" && ["approve", "import", "revoke", "rotate"].includes(path[2] ?? "")) {
359
+ // `revoke --force` / `--local-only` only clears local records (nothing signed or sent): allowed.
360
+ const o = actionCommand.opts();
361
+ if (path[2] === "revoke" && (o.force === true || o.localOnly === true))
362
+ return;
363
+ venueArgs = actionCommand.args.slice(0, 1);
364
+ feature = path[2] === "import" ? "agentImport" : `agent ${path[2]}`;
365
+ }
366
+ else {
367
+ return;
368
+ }
369
+ for (const v of venueArgs) {
370
+ const id = tryResolveVenueId(v);
371
+ if (id !== null && !getVenue(id).roles.includes("trading"))
372
+ throw roleUnsupported(getVenue(id), feature);
373
+ }
374
+ }
375
+ /** `perp a b c` path of a command (root name excluded). */
376
+ function commandPath(cmd) {
377
+ const names = [];
378
+ for (let c = cmd; c !== null && c.parent !== null; c = c.parent)
379
+ names.unshift(c.name());
380
+ return names.join(" ");
381
+ }
382
+ /**
383
+ * `perp help <group> <leaf> ...` shows the help of the deepest named command. Commander's built-in
384
+ * root help command resolves only its first operand, so `perp help market list` printed the
385
+ * `market` group help. Groups keep their own built-in `help` subcommand (`perp market help list`).
386
+ */
387
+ function installNestedHelp(program) {
388
+ program.helpCommand(false);
389
+ program
390
+ .command("help [command...]")
391
+ .description("display help for command (any depth: perp help trade limit)")
392
+ .action((names) => {
393
+ let cmd = program;
394
+ for (const name of names) {
395
+ const sub = cmd.commands.find(c => c.name() === name || c.aliases().includes(name));
396
+ if (sub === undefined) {
397
+ const where = cmd === program ? "perp" : `perp ${commandPath(cmd)}`;
398
+ throw new PerpError("INVALID_PARAMS", `Unknown command '${name}' under '${where}'`, {
399
+ remediation: `Available: ${cmd.commands.map(c => c.name()).join(", ")} — see: ${where} --help`,
400
+ });
401
+ }
402
+ cmd = sub;
403
+ }
404
+ cmd.help();
405
+ });
406
+ }
407
+ /**
408
+ * A command group run without a subcommand (`perp history`): Commander prints the group's help as
409
+ * an error. In --json mode that reached stdout as a CLI_ERROR whose message was the usage text;
410
+ * it is now one INVALID_PARAMS document naming the subcommands, with remediation (exit 1).
411
+ * Human mode keeps the usage listing on stderr.
412
+ */
413
+ function installMissingSubcommandError(program) {
414
+ program.on("beforeAllHelp", (ctx) => {
415
+ if (!ctx.error || program.opts().json !== true)
416
+ return;
417
+ const path = `perp ${commandPath(ctx.command)}`.trim();
418
+ const subs = ctx.command.commands.map(c => c.name()).filter(n => n !== "help");
419
+ throw new PerpError("INVALID_PARAMS", `'${path}' needs a subcommand: ${subs.join(", ")}`, {
420
+ remediation: `Run one of them, e.g. ${path} ${subs[0]} — list them with: ${path} --help`,
421
+ });
422
+ });
423
+ }