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,49 +1,262 @@
1
- import { makeTable, formatUsd, formatPercent, printJson, jsonOk, jsonError, withJsonErrors } from "../utils.js";
1
+ import { sortByTime, tapeSkippedOf } from "../exchanges/interface.js";
2
+ import { makeTable, formatUsd, formatPercent, printJson, jsonOk, jsonError, withJsonErrors, normalizeSymbolArg } from "../utils.js";
2
3
  import chalk from "chalk";
3
4
  import { PerpError } from "../errors.js";
4
5
  import { midDecimal } from "../exchanges/precision.js";
5
- import { hasPacificaSdk, isDexCapable, klinesNeedAuth } from "../exchanges/capabilities.js";
6
- import { listVenuesWithRole } from "../exchanges/venue.js";
6
+ import { isDexCapable, klinesNeedAuth } from "../exchanges/capabilities.js";
7
+ import { getVenue, listVenuesWithRole, tryResolveVenueId, unofficialApiNote, withFundingVerified } from "../exchanges/venue.js";
7
8
  import "../exchanges/venues/index.js";
8
- /** Venues with an adapter (market-data-only venues have none — see `perp arb scan`). */
9
- const EXCHANGES = listVenuesWithRole("account").map(v => v.id);
9
+ /**
10
+ * Venues of the multi-venue `market list|prices` (no -e): every venue with the marketData role
11
+ * and a (public) adapter — market-data-only venues (TxFlow, Variational, GRVT, Extended) included.
12
+ * They were left out in 0.15.0 because the list was the account-role venues.
13
+ */
14
+ export const MARKET_DATA_VENUES = listVenuesWithRole("marketData").filter(v => v.create !== null).map(v => v.id);
15
+ /**
16
+ * `--limit` / `-l` / `--depth`: a positive integer, the same rule on every venue (parseInt took
17
+ * "abc" as NaN and "-5" as a negative slice). Anything else → INVALID_PARAMS before any request.
18
+ */
19
+ export function parsePositiveIntArg(raw, flag) {
20
+ const t = raw.trim();
21
+ if (!/^\d+$/.test(t) || Number(t) < 1 || !Number.isSafeInteger(Number(t))) {
22
+ throw new PerpError("INVALID_PARAMS", `${flag} must be a positive integer (got "${raw}")`, { remediation: `Pass e.g. ${flag} 10` });
23
+ }
24
+ return Number(t);
25
+ }
26
+ /**
27
+ * Most order-book levels per side each venue serves (measured live 2026-10-19; the request size
28
+ * where the venue takes one): one rule for every venue — `--depth` above it is INVALID_PARAMS
29
+ * naming the max, never clamped or silently short. Absent = the full book (Extended).
30
+ */
31
+ export const BOOK_DEPTH_MAX = {
32
+ hyperliquid: 20, pacifica: 10, lighter: 50, "lighter-rh": 50, aster: 50, arcus: 50, ondo: 50,
33
+ qfex: 20, txflow: 20, risex: 250, grvt: 500,
34
+ };
35
+ export function assertBookDepth(venue, depth) {
36
+ const id = tryResolveVenueId(venue);
37
+ const max = id === null ? undefined : BOOK_DEPTH_MAX[id];
38
+ if (max !== undefined && depth > max) {
39
+ throw new PerpError("INVALID_PARAMS", `--depth ${depth} exceeds ${venue}'s order book (max ${max} levels per side)`, {
40
+ exchange: venue, remediation: `Pass --depth 1..${max}`,
41
+ });
42
+ }
43
+ }
44
+ /**
45
+ * `--start` / `--end`: unix ms (digits) or an ISO-8601 date / time (`Date.parse`; "2026-10-18" is
46
+ * UTC midnight). parseInt("2026-10-18") read 2026 ms. Non-finite → INVALID_PARAMS.
47
+ */
48
+ export function parseTimeArg(raw, flag) {
49
+ const t = raw.trim();
50
+ const ms = /^\d+$/.test(t) ? Number(t) : /^\d{4}-\d{2}-\d{2}/.test(t) ? Date.parse(t) : NaN;
51
+ // A unix-seconds value (10 digits today) read as ms lands in 1970: refused with a hint.
52
+ if (/^\d+$/.test(t) && ms < 1e11) {
53
+ throw new PerpError("INVALID_PARAMS", `${flag} ${raw} looks like unix seconds; pass milliseconds or an ISO date`, {
54
+ remediation: `Pass e.g. ${flag} ${t}000 or ${flag} 2026-10-18T00:00:00Z`,
55
+ });
56
+ }
57
+ if (!Number.isFinite(ms)) {
58
+ throw new PerpError("INVALID_PARAMS", `${flag} must be unix milliseconds or an ISO date (got "${raw}")`, {
59
+ remediation: `Pass e.g. ${flag} 1792368000000 or ${flag} 2026-10-18T00:00:00Z`,
60
+ });
61
+ }
62
+ return ms;
63
+ }
64
+ /**
65
+ * Bare market symbol of a CLI argument as `market info` matches it against getMarkets() rows:
66
+ * "btc-perp" → "BTC", Extended "BTC-USD" / "TSLA_24_5-USD" → "BTC" / "TSLA", TxFlow "BTC-USDC" → "BTC"
67
+ * (book / funding resolve these through the venue; info read the raw argument and missed them).
68
+ * HIP-3 "xyz:XYZ100" keeps its prefix.
69
+ */
70
+ export function bareSymbolOf(arg) {
71
+ return normalizeSymbolArg(arg.trim()).replace(/-PERP$/, "").replace(/-(USD|USDC|USDT)$/, "").replace(/_24_5$/, "");
72
+ }
73
+ /**
74
+ * History order, one rule for every venue: trades and funding history newest first, candles
75
+ * oldest first (Aster / QFEX answered funding oldest first, GRVT trades oldest first). A stable
76
+ * copy — the adapter's array (and its non-enumerable tape metadata) is left as is.
77
+ */
78
+ export function newestFirst(rows) {
79
+ return sortByTime(rows, "desc");
80
+ }
81
+ /** Human reason of a listed-but-untradable market, keyed on `status` (null when none is known). */
82
+ const NOT_TRADABLE_REASON = {
83
+ nonUsdQuote: "non-USD quote — readable, not tradable through perp-cli",
84
+ offHours: "outside the underlying's trading hours",
85
+ reduceOnly: "reduce-only",
86
+ halted: "trading halted",
87
+ };
88
+ export function notTradableReason(m) {
89
+ if (m.tradable !== false)
90
+ return null;
91
+ if (typeof m.status === "string" && m.status !== "")
92
+ return NOT_TRADABLE_REASON[m.status] ?? m.status;
93
+ return "not tradable right now";
94
+ }
95
+ /** Symbol cell; a listed market that is not tradable is marked with its reason. */
96
+ function symbolCell(m) {
97
+ const why = notTradableReason(m);
98
+ return why === null ? chalk.white.bold(m.symbol) : `${chalk.white.bold(m.symbol)} ${chalk.yellow(`(${why})`)}`;
99
+ }
100
+ /** Multi-venue human output: the footnote of every listed venue read through an unofficial API. */
101
+ function printUnofficialNotes(venues) {
102
+ for (const ex of venues) {
103
+ const id = tryResolveVenueId(ex);
104
+ const note = id === null ? null : unofficialApiNote(getVenue(id));
105
+ if (note !== null)
106
+ console.log(chalk.yellow(` * ${note}`));
107
+ }
108
+ }
109
+ /**
110
+ * meta.warnings for market rows whose fundingRate is null for a known reason (Lighter: the
111
+ * settled-rate snapshot could not be read) — the reason reaches --json, not only stderr.
112
+ */
113
+ function fundingWarnings(adapters, rows = null) {
114
+ // Tied to the rows served: no warning next to rows that all carry a rate (a cached listing).
115
+ if (rows !== null && !rows.some(r => r.fundingRate === null))
116
+ return undefined;
117
+ const w = adapters.map(a => a.fundingWarning?.() ?? null).filter((x) => x !== null);
118
+ return w.length > 0 ? { warnings: w } : undefined;
119
+ }
10
120
  /** null = venue does not publish the value → "-" (never rendered as $0). */
11
121
  const usdOrDash = (v) => (v === null ? chalk.gray("-") : `$${formatUsd(v)}`);
122
+ /** Non-USD quote currency of a row (QFEX "SAMSUNG-KRW": quoteAsset "KRW"); null = USD or not stated. */
123
+ const nonUsdQuoteOf = (m) => {
124
+ const q = m.quoteAsset;
125
+ return typeof q === "string" && q !== "USD" ? q : null;
126
+ };
127
+ /** A price of row `m` in its quote currency: "$1.23" for USD, "213,500 KRW" otherwise; null → "-". */
128
+ const priceOrDash = (v, m) => {
129
+ const q = nonUsdQuoteOf(m);
130
+ return v === null ? chalk.gray("-") : q === null ? `$${formatUsd(v)}` : `${formatUsd(v)} ${q}`;
131
+ };
132
+ /** 24h volume: USD, or (no USD figure) the venue's quote-currency notional labelled with its currency. */
133
+ const volumeCell = (m) => {
134
+ const local = m.volume24hQuote;
135
+ return m.volume24h === null && typeof local === "string" ? priceOrDash(local, m) : usdOrDash(m.volume24h);
136
+ };
137
+ /**
138
+ * Funding cell. On a venue whose funding interval is per market (VenueDefinition.fundingRateBasis
139
+ * "perMarket": Variational, Aster, GRVT) the raw rate is labelled with its interval ("+0.0079%/8h"), since
140
+ * the rows of one table cover different periods.
141
+ */
142
+ function fundingCell(m, perMarket) {
143
+ if (m.fundingRate == null)
144
+ return chalk.gray("N/A");
145
+ const hours = perMarket ? m.fundingHours ?? null : null;
146
+ return hours === null ? formatPercent(m.fundingRate) : `${formatPercent(m.fundingRate)}${chalk.gray(`/${hours}h`)}`;
147
+ }
148
+ /** True when `venueName` is a registered venue whose funding interval is per market. */
149
+ function perMarketFunding(venueName) {
150
+ const id = tryResolveVenueId(venueName);
151
+ return id !== null && getVenue(id).fundingRateBasis === "perMarket";
152
+ }
153
+ /** Base asset of a market symbol: "BTC-PERP" → "BTC", "xyz:XYZ100" → "XYZ100". */
154
+ const baseAssetOf = (symbol) => symbol.split(":").pop().replace(/-PERP$/i, "");
155
+ /**
156
+ * Open interest is published in BASE-asset units by every venue (ExchangeMarketInfo.openInterest),
157
+ * so it is shown as "<n> <BASE>" — never with a "$". `withUsd` adds the notional at the mark
158
+ * price, labelled as such; null (not published) → "-".
159
+ */
160
+ function formatOpenInterest(m, withUsd) {
161
+ if (m.openInterest === null)
162
+ return chalk.gray("-");
163
+ const oi = Number(m.openInterest);
164
+ const base = `${Number.isFinite(oi) ? oi.toLocaleString("en-US", { maximumFractionDigits: 4 }) : m.openInterest} ${baseAssetOf(m.symbol)}`;
165
+ const mark = Number(m.markPrice);
166
+ if (!withUsd || !Number.isFinite(oi) || !(mark > 0))
167
+ return base;
168
+ return `${base} ${chalk.gray(`(≈ ${priceOrDash(String(oi * mark), m)} at mark)`)}`;
169
+ }
170
+ function printRfqQuotes(q, json) {
171
+ if (json)
172
+ return printJson(jsonOk(q));
173
+ console.log(chalk.cyan.bold(`\n RFQ quotes: ${q.ticker} (${q.venue}) — not an order book\n`));
174
+ console.log(` Mark: $${formatUsd(q.markPrice)} Base spread: ${q.baseSpreadBps === null ? chalk.gray("-") : `${Number(q.baseSpreadBps).toFixed(2)} bps`} Quoted at: ${q.updatedAt ?? chalk.gray("-")}`);
175
+ if (q.quotes.length === 0) {
176
+ console.log(chalk.gray("\n No quotes published right now (the market may be closed).\n"));
177
+ }
178
+ else {
179
+ console.log(makeTable(["Size", "Bid", "Ask", "Spread"], q.quotes.map(t => {
180
+ const bid = Number(t.bid);
181
+ const ask = Number(t.ask);
182
+ const mid = (bid + ask) / 2;
183
+ return [
184
+ t.notionalUsd === null ? "min order" : `$${formatUsd(t.notionalUsd)}`,
185
+ chalk.green(`$${formatUsd(t.bid)}`),
186
+ chalk.red(`$${formatUsd(t.ask)}`),
187
+ mid > 0 ? `${((ask - bid) / mid * 10_000).toFixed(2)} bps` : chalk.gray("-"),
188
+ ];
189
+ })));
190
+ }
191
+ console.log(chalk.gray(` ${q.note}\n`));
192
+ }
193
+ function printFundingTerms(t, json) {
194
+ if (json)
195
+ return printJson(jsonOk(t));
196
+ console.log(chalk.cyan.bold(`\n Funding: ${t.ticker} (${t.venue}) — current terms, no history published\n`));
197
+ if (t.swap !== null) {
198
+ const row = (label, r) => r === null
199
+ ? [label, chalk.gray("-"), chalk.gray("-"), chalk.gray("-"), chalk.gray("-")]
200
+ : [label, formatPercent(r.intervalLong), formatPercent(r.intervalShort), formatPercent(r.annualizedLong), formatPercent(r.annualizedShort)];
201
+ console.log(makeTable(["Payment", "Long / day", "Short / day", "Long APR", "Short APR"], [row("last applied", t.swap.last), row("next", t.swap.next)]));
202
+ }
203
+ else {
204
+ console.log(` Rate kind: ${t.rateKind ?? chalk.gray("-")}`);
205
+ console.log(` Interval: ${t.intervalHours === null ? chalk.gray("-") : `${t.intervalHours}h`}`);
206
+ console.log(` Per interval: ${t.intervalRate === null ? chalk.gray("N/A") : formatPercent(t.intervalRate)}`);
207
+ console.log(` Hourly: ${t.hourlyRate === null ? chalk.gray("N/A") : formatPercent(t.hourlyRate)}`);
208
+ console.log(` APR: ${t.annualizedPct === null ? chalk.gray("N/A") : `${t.annualizedPct.toFixed(2)}%`}`);
209
+ }
210
+ console.log(` Next funding: ${t.nextFundingTime === null ? chalk.gray("-") : new Date(t.nextFundingTime).toLocaleString()}`);
211
+ console.log(chalk.gray(` ${t.note}\n`));
212
+ }
12
213
  export function registerMarketCommands(program, getAdapter, isJson, getAdapterForExchange,
13
214
  /** Credentialed adapter of the selected venue — for market data the venue serves only to an authenticated account. */
14
215
  getAccountAdapter) {
15
216
  const market = program.command("market").description("Market data commands");
217
+ // Human output for a venue read through an UNOFFICIAL API carries a footnote (JSON rows carry venueApi).
218
+ market.hook("postAction", () => {
219
+ if (isJson())
220
+ return;
221
+ const raw = program.opts().exchange;
222
+ const id = raw === undefined ? null : tryResolveVenueId(raw);
223
+ if (id === null)
224
+ return;
225
+ const note = unofficialApiNote(getVenue(id));
226
+ if (note !== null)
227
+ console.log(chalk.yellow(` * ${note}\n`));
228
+ });
16
229
  market
17
230
  .command("list")
18
231
  .description("List all available markets")
19
232
  .option("--hip3", "Include HIP-3 dex markets (Hyperliquid)")
20
233
  .action(async (opts) => {
21
234
  const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
22
- const HEADERS = ["Symbol", "Mark", "Index", "Funding", "24h Vol", "OI", "Max Lev"];
23
- const marketRow = (m) => [
24
- chalk.white.bold(m.symbol),
25
- `$${formatUsd(m.markPrice)}`,
26
- usdOrDash(m.indexPrice),
27
- m.fundingRate != null ? formatPercent(m.fundingRate) : chalk.gray("N/A"),
28
- usdOrDash(m.volume24h),
29
- usdOrDash(m.openInterest),
235
+ const HEADERS = ["Symbol", "Mark", "Index", "Funding", "24h Vol", "OI (base)", "Max Lev"];
236
+ const marketRow = (m, perMarket = false) => [
237
+ symbolCell(m),
238
+ priceOrDash(m.markPrice, m),
239
+ priceOrDash(m.indexPrice, m),
240
+ fundingCell(m, perMarket),
241
+ volumeCell(m),
242
+ formatOpenInterest(m, false),
30
243
  m.maxLeverage === null ? chalk.gray("-") : String(m.maxLeverage) + "x",
31
244
  ];
32
245
  // Multi-exchange mode
33
246
  if (!explicitExchange && getAdapterForExchange) {
34
247
  const grouped = {};
35
248
  const errors = {};
36
- await Promise.all(EXCHANGES.map(async (ex) => {
249
+ const listed = [];
250
+ await Promise.all(MARKET_DATA_VENUES.map(async (ex) => {
37
251
  try {
38
252
  const adapter = await getAdapterForExchange(ex);
39
253
  grouped[ex] = await adapter.getMarkets();
254
+ listed.push(adapter);
40
255
  // --hip3: fetch HIP-3 dex markets
41
256
  if (opts.hip3 && ex === "hyperliquid" && isDexCapable(adapter)) {
42
257
  const dexes = await adapter.listDeployedDexes();
43
258
  await Promise.allSettled(dexes.map(async (d) => {
44
- const dexAdapter = Object.create(adapter);
45
- dexAdapter.setDex(d.name);
46
- const markets = await dexAdapter.getMarkets();
259
+ const markets = await adapter.forDex(d.name).getMarkets();
47
260
  if (markets.length > 0)
48
261
  grouped[`hip3:${d.name}`] = markets;
49
262
  }));
@@ -54,7 +267,7 @@ getAccountAdapter) {
54
267
  }
55
268
  }));
56
269
  if (isJson()) {
57
- return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
270
+ return printJson(jsonOk({ exchanges: Object.fromEntries(Object.entries(grouped).map(([ex, rows]) => [ex, withFundingVerified(ex, rows)])), errors: Object.keys(errors).length > 0 ? errors : undefined }, fundingWarnings(listed, Object.values(grouped).flat())));
58
271
  }
59
272
  // Per-exchange tables
60
273
  for (const [ex, markets] of Object.entries(grouped)) {
@@ -63,11 +276,12 @@ getAccountAdapter) {
63
276
  const isHip3 = ex.startsWith("hip3:");
64
277
  const label = isHip3 ? chalk.magenta.bold(` HIP-3: ${ex.slice(5)}`) : chalk.cyan.bold(` ${ex.toUpperCase()}`);
65
278
  console.log(`\n${label} ${chalk.gray(`(${markets.length} markets)`)}`);
66
- console.log(makeTable(HEADERS, markets.map(marketRow)));
279
+ console.log(makeTable(HEADERS, markets.map(m => marketRow(m))));
67
280
  }
68
281
  for (const [ex, msg] of Object.entries(errors)) {
69
282
  console.log(chalk.gray(` ${ex}: ${msg}`));
70
283
  }
284
+ printUnofficialNotes(Object.keys(grouped));
71
285
  return;
72
286
  }
73
287
  // Single exchange mode
@@ -78,23 +292,32 @@ getAccountAdapter) {
78
292
  if (opts.hip3 && isDexCapable(adapter)) {
79
293
  const dexes = await adapter.listDeployedDexes();
80
294
  await Promise.allSettled(dexes.map(async (d) => {
81
- const dexAdapter = Object.create(adapter);
82
- dexAdapter.setDex(d.name);
83
- const m = await dexAdapter.getMarkets();
295
+ const m = await adapter.forDex(d.name).getMarkets();
84
296
  if (m.length > 0)
85
297
  hip3[d.name] = m;
86
298
  }));
87
299
  }
88
300
  if (isJson()) {
89
301
  if (Object.keys(hip3).length > 0) {
90
- return printJson(jsonOk({ main: markets, hip3 }));
302
+ return printJson(jsonOk({ main: withFundingVerified(adapter.name, markets), hip3: Object.fromEntries(Object.entries(hip3).map(([d, rows]) => [d, withFundingVerified(adapter.name, rows)])) }));
91
303
  }
92
- return printJson(jsonOk(markets));
304
+ return printJson(jsonOk(withFundingVerified(adapter.name, markets), fundingWarnings([adapter], markets)));
305
+ }
306
+ const perMarket = perMarketFunding(adapter.name);
307
+ // Rows that carry an asset class (Extended: crypto / equity / etf / index / commodity / fx / preIpo) show it.
308
+ const classOf = (m) => {
309
+ const c = m.assetClass;
310
+ return typeof c === "string" ? c : null;
311
+ };
312
+ if (markets.length > 0 && markets.every(m => classOf(m) !== null)) {
313
+ console.log(makeTable([...HEADERS, "Class"], markets.map(m => [...marketRow(m, perMarket), classOf(m)])));
314
+ }
315
+ else {
316
+ console.log(makeTable(HEADERS, markets.map(m => marketRow(m, perMarket))));
93
317
  }
94
- console.log(makeTable(HEADERS, markets.map(marketRow)));
95
318
  for (const [dex, m] of Object.entries(hip3)) {
96
319
  console.log(chalk.magenta.bold(`\n HIP-3: ${dex}`) + chalk.gray(` (${m.length} markets)`));
97
- console.log(makeTable(HEADERS, m.map(marketRow)));
320
+ console.log(makeTable(HEADERS, m.map(x => marketRow(x))));
98
321
  }
99
322
  });
100
323
  market
@@ -103,98 +326,109 @@ getAccountAdapter) {
103
326
  .action(async () => {
104
327
  const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
105
328
  const PRICE_HEADERS = ["Symbol", "Mark", "Index", "Funding"];
106
- const priceRow = (m) => [
107
- chalk.white.bold(m.symbol),
108
- `$${formatUsd(m.markPrice)}`,
109
- usdOrDash(m.indexPrice),
110
- m.fundingRate != null ? formatPercent(m.fundingRate) : chalk.gray("N/A"),
329
+ const priceRow = (m, perMarket = false) => [
330
+ symbolCell(m),
331
+ priceOrDash(m.markPrice, m),
332
+ priceOrDash(m.indexPrice, m),
333
+ fundingCell(m, perMarket),
111
334
  ];
112
335
  if (!explicitExchange && getAdapterForExchange) {
113
336
  const grouped = {};
114
337
  const errors = {};
115
- await Promise.all(EXCHANGES.map(async (ex) => {
338
+ const listed = [];
339
+ await Promise.all(MARKET_DATA_VENUES.map(async (ex) => {
116
340
  try {
117
341
  const adapter = await getAdapterForExchange(ex);
118
342
  grouped[ex] = await adapter.getMarkets();
343
+ listed.push(adapter);
119
344
  }
120
345
  catch (err) {
121
346
  errors[ex] = err instanceof Error ? err.message : String(err);
122
347
  }
123
348
  }));
124
349
  if (isJson()) {
125
- return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
350
+ return printJson(jsonOk({ exchanges: Object.fromEntries(Object.entries(grouped).map(([ex, rows]) => [ex, withFundingVerified(ex, rows)])), errors: Object.keys(errors).length > 0 ? errors : undefined }, fundingWarnings(listed, Object.values(grouped).flat())));
126
351
  }
127
352
  for (const [ex, markets] of Object.entries(grouped)) {
128
353
  if (markets.length === 0)
129
354
  continue;
130
355
  console.log(chalk.cyan.bold(`\n ${ex.toUpperCase()}`) + chalk.gray(` (${markets.length} markets)`));
131
- console.log(makeTable(PRICE_HEADERS, markets.map(priceRow)));
356
+ console.log(makeTable(PRICE_HEADERS, markets.map(m => priceRow(m))));
132
357
  }
133
358
  for (const [ex, msg] of Object.entries(errors))
134
359
  console.log(chalk.gray(` ${ex}: ${msg}`));
360
+ printUnofficialNotes(Object.keys(grouped));
135
361
  return;
136
362
  }
137
363
  const adapter = await getAdapter();
138
- if (hasPacificaSdk(adapter)) {
139
- const sdk = adapter.sdk;
140
- const prices = await sdk.getPrices();
141
- if (isJson())
142
- return printJson(jsonOk(prices));
143
- const rows = prices.map((p) => [
144
- chalk.white.bold(p.symbol),
145
- `$${formatUsd(p.mark)}`,
146
- `$${formatUsd(p.mid)}`,
147
- `$${formatUsd(p.oracle)}`,
148
- formatPercent(p.funding),
149
- ]);
150
- console.log(makeTable(["Symbol", "Mark", "Mid", "Oracle", "Funding"], rows));
151
- }
152
- else {
153
- const markets = await adapter.getMarkets();
154
- if (isJson())
155
- return printJson(jsonOk(markets));
156
- console.log(makeTable(PRICE_HEADERS, markets.map(priceRow)));
157
- }
364
+ // One normalized shape on every venue (Pacifica included — it printed the raw /info/prices rows).
365
+ const markets = await adapter.getMarkets();
366
+ if (isJson())
367
+ return printJson(jsonOk(withFundingVerified(adapter.name, markets), fundingWarnings([adapter], markets)));
368
+ console.log(makeTable(PRICE_HEADERS, markets.map(m => priceRow(m, perMarketFunding(adapter.name)))));
158
369
  });
159
370
  market
160
371
  .command("info <symbol>")
161
372
  .description("Detailed info for a single market")
162
373
  .action(async (symbol) => {
163
374
  const adapter = await getAdapter();
375
+ // A venue that serves one market directly (GRVT: one ticker + one margin-rules read instead
376
+ // of the whole fan-out) answers here; SYMBOL_NOT_FOUND comes from its own directory.
377
+ const single = adapter.getMarketInfo;
378
+ if (typeof single === "function") {
379
+ return void await withJsonErrors(isJson(), async () => {
380
+ const one = await single.call(adapter, symbol);
381
+ if (isJson())
382
+ return printJson(jsonOk(withFundingVerified(adapter.name, [one])[0]));
383
+ printMarketInfo(adapter.name, one);
384
+ });
385
+ }
164
386
  const markets = await adapter.getMarkets();
165
387
  const sym = symbol.toUpperCase();
388
+ const bare = bareSymbolOf(symbol).toUpperCase();
166
389
  const m = markets.find((mk) => {
167
390
  const s = mk.symbol.toUpperCase();
168
- return s === sym || s === `${sym}-PERP` || s.replace(/-PERP$/, "") === sym
169
- || s.split(":").pop() === sym; // HIP-3: "km:US500" matches "US500"
391
+ const sBare = s.replace(/-PERP$/, "");
392
+ return s === sym || sBare === sym || sBare === bare
393
+ || s.split(":").pop() === sym || s.split(":").pop() === bare; // HIP-3: "km:US500" matches "US500"
170
394
  });
171
395
  if (!m) {
172
- if (isJson())
173
- return printJson(jsonError("SYMBOL_NOT_FOUND", `Market ${sym} not found`));
174
- console.log(chalk.red(`\n Market "${sym}" not found.\n`));
175
- return process.exit(1);
396
+ // Printed (envelope / Error + Remediation) and exit 1 via withJsonErrors.
397
+ return void await withJsonErrors(isJson(), async () => {
398
+ const delistedErr = adapter.delistedError?.(symbol) ?? null;
399
+ if (delistedErr !== null)
400
+ throw delistedErr;
401
+ const delisted = typeof adapter.delistedMessage === "function"
402
+ ? adapter.delistedMessage(symbol)
403
+ : null;
404
+ throw new PerpError("SYMBOL_NOT_FOUND", delisted ?? `Market ${normalizeSymbolArg(symbol)} not found on ${adapter.name}`, {
405
+ exchange: adapter.name,
406
+ remediation: `List markets: perp -e ${adapter.name} market list`,
407
+ });
408
+ });
176
409
  }
177
410
  if (isJson())
178
- return printJson(jsonOk(m));
179
- console.log(chalk.cyan.bold(`\n ${m.symbol} on ${adapter.name}\n`));
180
- console.log(` Mark Price: $${formatUsd(m.markPrice)}`);
181
- console.log(` Index Price: ${usdOrDash(m.indexPrice)}`);
182
- console.log(` Funding Rate: ${m.fundingRate != null ? formatPercent(m.fundingRate) : chalk.gray("N/A")}`);
183
- console.log(` 24h Volume: ${usdOrDash(m.volume24h)}`);
184
- console.log(` Open Interest: ${usdOrDash(m.openInterest)}`);
185
- console.log(` Max Leverage: ${m.maxLeverage === null ? chalk.gray("-") : `${m.maxLeverage}x`}`);
186
- console.log();
411
+ return printJson(jsonOk(withFundingVerified(adapter.name, [m])[0], fundingWarnings([adapter], [m])));
412
+ printMarketInfo(adapter.name, m);
187
413
  });
188
414
  market
189
415
  .command("book <symbol>")
190
416
  .description("Show orderbook for a symbol")
191
417
  .option("-d, --depth <n>", "Number of levels", "10")
192
418
  .action(async (symbol, opts) => {
419
+ const depth = parsePositiveIntArg(opts.depth, "--depth");
193
420
  const adapter = await getAdapter();
194
- const book = await adapter.getOrderbook(symbol.toUpperCase());
421
+ const sym = normalizeSymbolArg(symbol);
422
+ // RFQ venue (Variational): no order book — the maker's indicative quotes, labelled as such.
423
+ if (adapter.getRfqQuotes !== undefined)
424
+ return printRfqQuotes(await adapter.getRfqQuotes(sym), isJson());
425
+ // `depth` reaches venues that size the request by it (GRVT, RISEx); every answer is cut to
426
+ // `depth` levels per side — JSON included (it printed the venue's full book).
427
+ assertBookDepth(adapter.name, depth);
428
+ const full = await adapter.getOrderbook(sym, depth);
429
+ const book = { ...full, bids: full.bids.slice(0, depth), asks: full.asks.slice(0, depth) };
195
430
  if (isJson())
196
431
  return printJson(jsonOk(book));
197
- const depth = parseInt(opts.depth);
198
432
  const asks = book.asks.slice(0, depth).reverse();
199
433
  const bids = book.bids.slice(0, depth);
200
434
  // Compute spread
@@ -214,22 +448,25 @@ getAccountAdapter) {
214
448
  const len = maxCum > 0 ? Math.round((cum / maxCum) * BAR_WIDTH) : 0;
215
449
  return "█".repeat(len);
216
450
  };
217
- console.log(chalk.cyan.bold(`\n Orderbook: ${symbol.toUpperCase()} (${adapter.name})\n`));
218
- console.log(chalk.gray(" Price Size Cum$ Depth"));
451
+ console.log(chalk.cyan.bold(`\n Orderbook: ${sym} (${adapter.name})\n`));
452
+ // A book with a non-USD quoteAsset (QFEX SAMSUNG-KRW) shows its currency, never a $.
453
+ const quote = nonUsdQuoteOf(book);
454
+ const money = (v) => priceOrDash(String(v), book);
455
+ console.log(chalk.gray(` Price Size ${quote === null ? "Cum$ " : `Cum (${quote})`.padEnd(14)} Depth`));
219
456
  console.log(chalk.gray(" " + "─".repeat(60)));
220
457
  asks.forEach(([p, a], i) => {
221
458
  const cumUsd = askCums[i] * (Number(p));
222
- console.log(chalk.red(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)} $${formatUsd(cumUsd).padEnd(14)}`) +
459
+ console.log(chalk.red(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)} ${money(cumUsd).padEnd(quote === null ? 15 : 18)}`) +
223
460
  chalk.red.dim(bar(askCums[i])));
224
461
  });
225
462
  // Spread line
226
463
  const spreadStr = spreadAbs > 0
227
- ? `$${formatUsd(spreadAbs)} (${spreadPct.toFixed(3)}%)`
464
+ ? `${money(spreadAbs)} (${spreadPct.toFixed(3)}%)`
228
465
  : "─";
229
466
  console.log(chalk.gray(` ${"─".repeat(16)} spread: ${spreadStr} ${"─".repeat(10)}`));
230
467
  bids.forEach(([p, a], i) => {
231
468
  const cumUsd = bidCums[i] * (Number(p));
232
- console.log(chalk.green(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)} $${formatUsd(cumUsd).padEnd(14)}`) +
469
+ console.log(chalk.green(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)} ${money(cumUsd).padEnd(quote === null ? 15 : 18)}`) +
233
470
  chalk.green.dim(bar(bidCums[i])));
234
471
  });
235
472
  console.log();
@@ -237,12 +474,19 @@ getAccountAdapter) {
237
474
  market
238
475
  .command("trades <symbol>")
239
476
  .description("Recent trades for a symbol")
240
- .option("--limit <n>", "Number of recent trades to show", "20")
477
+ .option("--limit <n>", "Number of recent trades to show (default: the venue's own default, within its cap)")
241
478
  .action(async (symbol, opts) => {
479
+ // No --limit → undefined: each adapter applies its own default within its venue cap, so a
480
+ // plain `market trades` never trips a cap (HL serves 10).
481
+ const limit = opts.limit === undefined ? undefined : parsePositiveIntArg(opts.limit, "--limit");
242
482
  const adapter = await getAdapter();
243
- const trades = await adapter.getRecentTrades(symbol.toUpperCase(), parseInt(opts.limit));
483
+ const raw = await adapter.getRecentTrades(normalizeSymbolArg(symbol), limit);
484
+ const trades = newestFirst(raw);
485
+ const skipped = tapeSkippedOf(trades); // metadata survives the sort (sortByTime)
244
486
  if (isJson())
245
- return printJson(jsonOk(trades));
487
+ return printJson(jsonOk(trades, skipped !== null ? { skippedRows: skipped } : undefined));
488
+ if (skipped !== null)
489
+ console.log(chalk.yellow(` ${skipped.count} tape row(s) skipped: ${skipped.reason}`));
246
490
  if (trades.length === 0) {
247
491
  console.log(chalk.gray("\n No recent trades.\n"));
248
492
  return;
@@ -260,34 +504,41 @@ getAccountAdapter) {
260
504
  .description("Funding rate history")
261
505
  .option("-l, --limit <n>", "Number of records", "10")
262
506
  .action(async (symbol, opts) => {
507
+ const limit = parsePositiveIntArg(opts.limit, "--limit");
263
508
  const adapter = await getAdapter();
264
- const history = await adapter.getFundingHistory(symbol.toUpperCase(), parseInt(opts.limit));
509
+ // A venue without a funding-history API (Variational): its current funding terms, labelled as such.
510
+ if (adapter.getFundingTerms !== undefined)
511
+ return printFundingTerms(await adapter.getFundingTerms(normalizeSymbolArg(symbol)), isJson());
512
+ const history = newestFirst(await adapter.getFundingHistory(normalizeSymbolArg(symbol), limit));
265
513
  if (isJson())
266
514
  return printJson(jsonOk(history));
267
515
  if (history.length === 0) {
268
516
  console.log(chalk.gray("\n No funding history.\n"));
269
517
  return;
270
518
  }
271
- const hasOracle = history.some((h) => h.price != null && h.price !== "0" && h.price !== "");
519
+ // `price` is the venue's reference price of the print — oracle (Pacifica), mark (GRVT, Aster)
520
+ // or index (RISEx) — so the column is "Price", not "Oracle"; each rate shows its interval.
521
+ const hasPrice = history.some((h) => h.price != null && h.price !== "0" && h.price !== "");
272
522
  const rows = history.map((h) => {
273
523
  const base = [
274
524
  new Date(h.time).toLocaleString(),
275
- formatPercent(h.rate),
525
+ h.rate === null ? chalk.gray(h.marketClosed === true ? "market closed" : "N/A") : formatPercent(h.rate),
526
+ h.intervalHours === null ? chalk.gray("-") : `${h.intervalHours}h`,
276
527
  ];
277
- if (hasOracle)
528
+ if (hasPrice)
278
529
  base.push(h.price != null ? `$${formatUsd(h.price)}` : chalk.gray("n/a"));
279
530
  return base;
280
531
  });
281
- const headers = hasOracle ? ["Time", "Funding Rate", "Oracle"] : ["Time", "Funding Rate"];
532
+ const headers = hasPrice ? ["Time", "Funding Rate", "Interval", "Price"] : ["Time", "Funding Rate", "Interval"];
282
533
  console.log(makeTable(headers, rows));
283
- if (!hasOracle)
284
- console.log(chalk.gray(" Note: Oracle price not available for this exchange.\n"));
534
+ if (!hasPrice)
535
+ console.log(chalk.gray(" Note: no reference price published with this venue's funding prints.\n"));
285
536
  });
286
537
  market
287
538
  .command("mid <symbol>")
288
539
  .description("Get mid price for a single symbol (fast)")
289
540
  .action(async (symbol) => {
290
- const sym = symbol.toUpperCase();
541
+ const sym = normalizeSymbolArg(symbol);
291
542
  // withJsonErrors: every failure (incl. an empty book) is an error envelope AND exit code 1.
292
543
  await withJsonErrors(isJson(), async () => {
293
544
  const adapter = await getAdapter();
@@ -328,30 +579,38 @@ getAccountAdapter) {
328
579
  market
329
580
  .command("kline <symbol> <interval>")
330
581
  .description("Kline/candlestick data (intervals: 1m,5m,15m,1h,4h,1d,...)")
331
- .option("--start <ts>", "Start time (unix ms)")
332
- .option("--end <ts>", "End time (unix ms)")
582
+ .option("--start <ts>", "Start time (unix ms or ISO date, e.g. 2026-10-18T00:00:00Z)")
583
+ .option("--end <ts>", "End time (unix ms or ISO date)")
333
584
  .action(async (symbol, interval, opts) => {
334
585
  const publicAdapter = await getAdapter();
335
586
  // A venue whose candles endpoint is authenticated (Ondo) is read with the registered key.
336
587
  const adapter = getAccountAdapter !== undefined && klinesNeedAuth(publicAdapter) ? await getAccountAdapter() : publicAdapter;
337
588
  const now = Date.now();
338
- const startTime = opts.start ? parseInt(opts.start) : now - 24 * 60 * 60 * 1000;
339
- const endTime = opts.end ? parseInt(opts.end) : now;
340
- const klines = await adapter.getKlines(symbol.toUpperCase(), interval, startTime, endTime);
589
+ const startTime = opts.start !== undefined ? parseTimeArg(opts.start, "--start") : now - 24 * 60 * 60 * 1000;
590
+ const endTime = opts.end !== undefined ? parseTimeArg(opts.end, "--end") : now;
591
+ if (startTime > endTime) {
592
+ throw new PerpError("INVALID_PARAMS", `--start (${new Date(startTime).toISOString()}) is after --end (${new Date(endTime).toISOString()})`, {
593
+ remediation: "Pass --start before --end",
594
+ });
595
+ }
596
+ // Candles oldest first (ascending open time) on every venue.
597
+ const klines = sortByTime(await adapter.getKlines(normalizeSymbolArg(symbol), interval, startTime, endTime), "asc");
341
598
  if (isJson())
342
599
  return printJson(jsonOk(klines));
343
600
  if (klines.length === 0) {
344
601
  console.log(chalk.gray("\n No kline data.\n"));
345
602
  return;
346
603
  }
604
+ // ISO (UTC) candle open times — one format for every venue, independent of the host locale.
347
605
  const rows = klines.slice(-20).map((k) => [
348
- new Date(k.time).toLocaleString(),
349
- `$${formatUsd(k.open)}`,
350
- `$${formatUsd(k.high)}`,
351
- `$${formatUsd(k.low)}`,
352
- `$${formatUsd(k.close)}`,
353
- k.volume,
354
- String(k.trades),
606
+ new Date(k.time).toISOString(),
607
+ // A row with a non-USD quoteAsset (QFEX SAMSUNG-KRW) prints its currency, never a $.
608
+ priceOrDash(k.open, k),
609
+ priceOrDash(k.high, k),
610
+ priceOrDash(k.low, k),
611
+ priceOrDash(k.close, k),
612
+ k.volume === null ? chalk.gray("-") : k.volume,
613
+ k.trades === null ? chalk.gray("-") : String(k.trades),
355
614
  ]);
356
615
  console.log(makeTable(["Time", "Open", "High", "Low", "Close", "Volume", "Trades"], rows));
357
616
  });
@@ -380,7 +639,8 @@ getAccountAdapter) {
380
639
  const rows = dexes.map(d => [
381
640
  chalk.white.bold(d.name),
382
641
  chalk.gray(d.deployer ? d.deployer.slice(0, 6) + "..." + d.deployer.slice(-4) : "-"),
383
- String(d.assets.length),
642
+ // Live (non-delisted) assets; a dex whose every asset is delisted is marked as such.
643
+ d.delisted === true ? chalk.gray("delisted") : String(d.assets.length),
384
644
  d.assets.slice(0, 5).join(", ") + (d.assets.length > 5 ? ` +${d.assets.length - 5}` : ""),
385
645
  ]);
386
646
  console.log(makeTable(["DEX", "Deployer", "Assets", "Markets"], rows));
@@ -389,3 +649,38 @@ getAccountAdapter) {
389
649
  });
390
650
  });
391
651
  }
652
+ /**
653
+ * Why a listed market is not tradable, from the row's `status` when the venue states it (QFEX
654
+ * "nonUsdQuote", Ondo "closed" / "reduceOnly"); the generic off-hours text only when it does not.
655
+ * null = tradable or no tradability flag.
656
+ */
657
+ export function notTradableNote(m) {
658
+ const r = m;
659
+ if (r.tradable !== false)
660
+ return null;
661
+ if (r.status === "nonUsdQuote")
662
+ return `Read-only in perp-cli: quoted in ${typeof r.quoteAsset === "string" ? r.quoteAsset : "a non-USD currency"} (orders, sizing and risk limits are USD-only).`;
663
+ if (r.status === "closed")
664
+ return "Not tradable: the venue has closed this market to new trading.";
665
+ if (r.status === "reduceOnly")
666
+ return "Reduce-only on the venue: positions can be closed, not opened.";
667
+ return "Not tradable right now (off-hours, reduce-only or halted on the venue).";
668
+ }
669
+ function printMarketInfo(venue, m) {
670
+ console.log(chalk.cyan.bold(`\n ${m.symbol} on ${venue}\n`));
671
+ const note = notTradableNote(m);
672
+ if (note !== null)
673
+ console.log(chalk.yellow(` ${note}`));
674
+ console.log(` Mark Price: ${priceOrDash(m.markPrice, m)}`);
675
+ console.log(` Index Price: ${priceOrDash(m.indexPrice, m)}`);
676
+ // Per-market funding interval (GRVT 4h / 8h, Variational, Aster): the rate carries its interval.
677
+ console.log(` Funding Rate: ${fundingCell(m, perMarketFunding(venue))}`);
678
+ // A venue whose rate is unverified (QFEX) labels it on the row.
679
+ const fundingNote = m.fundingNote;
680
+ if (typeof fundingNote === "string")
681
+ console.log(chalk.yellow(` ${fundingNote}`));
682
+ console.log(` 24h Volume: ${volumeCell(m)}`);
683
+ console.log(` Open Interest: ${formatOpenInterest(m, true)}`);
684
+ console.log(` Max Leverage: ${m.maxLeverage === null ? chalk.gray("-") : `${m.maxLeverage}x`}`);
685
+ console.log();
686
+ }