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,5 +1,6 @@
1
+ import { assertCandleInterval } from "./interface.js";
1
2
  import { LocalEvmSigner } from "../signer/index.js";
2
- import { DEFINITE_ORDER_FAILURE_CODES, PerpError, classifyError, extractErrorMessage, orderNotSent, orderOutcomeUnknown, sendFailure } from "../errors.js";
3
+ import { DEFINITE_ORDER_FAILURE_CODES, PerpError, classifyError, extractErrorMessage, orderNotSent, orderOutcomeUnknown, sendFailure, noAccountKeyError } from "../errors.js";
3
4
  import { parseFiniteVenueNumber, venueRateField } from "../utils/numeric.js";
4
5
  import { alignProtectionPrice, floorSize, formatComputedPrice, slippagePrice, subDecimal } from "./precision.js";
5
6
  import { unsupported } from "./trading-v2.js";
@@ -9,9 +10,9 @@ import { rateLimitedError } from "./rate-limiter.js";
9
10
  import { fetchAndCache, withCache, TTL_ACCOUNT } from "../cache.js";
10
11
  import { approvalFromMax, builderGateOf, hlFeeTenthsBps, NO_BUILDER, requireFeeBps } from "./builder-fee.js";
11
12
  /**
12
- * Map a TIF input to HL's native value (design §13.3). Accepts the common names
13
+ * Map a TIF input to HL's native value. Accepts the common names
13
14
  * GTC/IOC/ALO and the native spellings case-insensitively. FOK is not offered by
14
- * HL (no FOK emulation, §13.3) → CAPABILITY_UNSUPPORTED; anything else likewise.
15
+ * HL (no FOK emulation) → CAPABILITY_UNSUPPORTED; anything else likewise.
15
16
  */
16
17
  export function hlTif(raw) {
17
18
  const t = raw.trim().toUpperCase();
@@ -31,7 +32,7 @@ export function hlSizeStep(szDecimals) {
31
32
  return szDecimals <= 0 ? "1" : `0.${"0".repeat(szDecimals - 1)}1`;
32
33
  }
33
34
  /**
34
- * Slippage-bounded IOC protection price (design §13.2, §13.10 #5).
35
+ * Slippage-bounded IOC protection price.
35
36
  *
36
37
  * `reference` × (1 ± bps/10000), then aligned TOWARD CROSSING (buy → up, sell →
37
38
  * down) on HL's price grid: at most 5 significant figures and at most
@@ -56,8 +57,8 @@ export function hlProtectionPrice(reference, side, bps, szDecimals, maxDecimals)
56
57
  return { protectionPrice, appliedBps };
57
58
  }
58
59
  /**
59
- * A USER-SUPPLIED limit / trigger / modify price checked against HL's price grid (design §10.10:
60
- * an off-grid limit price is refused, never rounded — rounding to nearest can move a buy price UP).
60
+ * A USER-SUPPLIED limit / trigger / modify price checked against HL's price grid:
61
+ * an off-grid limit price is refused, never rounded — rounding to nearest can move a buy price UP.
61
62
  * HL docs (trading/tick-and-lot-size): "Prices can have up to 5 significant figures, but no more
62
63
  * than MAX_DECIMALS - szDecimals decimal places where MAX_DECIMALS is 6 for perps and 8 for spot.
63
64
  * Integer prices are always allowed, regardless of the number of significant figures."
@@ -140,7 +141,7 @@ export async function assertHlAppliedBps(appliedBps, what) {
140
141
  }
141
142
  /**
142
143
  * Definite IOC outcome of a raw HL `order` response (shared contract with the
143
- * perp-order lane, design §13.10 #4). Reads `response.data.statuses[0]`:
144
+ * perp-order lane). Reads `response.data.statuses[0]`:
144
145
  * - `{ filled: { totalSz } }` with a plain decimal totalSz → that size, final;
145
146
  * - `{ error }` that is HL's IOC no-match cancel ("could not immediately match")
146
147
  * → the venue explicitly cancelled the IOC with nothing filled → "0", final;
@@ -270,6 +271,18 @@ export const HL_ABSTRACTION_TTL_MS = 60_000;
270
271
  export function hlAbstractionCachePrefix(network) {
271
272
  return `mode:hl:${network}:abstraction:`;
272
273
  }
274
+ /**
275
+ * Margin tiers of a universe entry. Docs "Margin tiers": "For IDs less than 50, there is a single
276
+ * tier with max leverage equal to the ID" — those tables are not in `marginTables`, so the
277
+ * implicit tier is emitted instead of null. Unknown ids ≥ 50 → null.
278
+ */
279
+ export function hlMarginTiersOf(marginTableId, tierMap) {
280
+ const id = Number(marginTableId);
281
+ const tiers = tierMap.get(id);
282
+ if (tiers !== undefined)
283
+ return tiers;
284
+ return Number.isInteger(id) && id > 0 && id < 50 ? [{ lowerBound: 0, maxLeverage: id }] : null;
285
+ }
273
286
  /** Venue margin restriction of a universe entry: strictIsolated (no margin removal) / noCross; null = unrestricted. */
274
287
  export function hlMarginModeOf(asset) {
275
288
  if (asset.marginMode === "strictIsolated")
@@ -343,6 +356,32 @@ export function hlL1ActionRejection(type, e) {
343
356
  const HL_FUNDING_PAGE = 500;
344
357
  /** 7 days of hourly funding = 168 rows per coin; 20 pages = 10 000 rows (~60 coins). */
345
358
  const HL_FUNDING_MAX_PAGES = 20;
359
+ /**
360
+ * candleSnapshot intervals (docs "Candle snapshot": "Supported intervals") with their nominal
361
+ * length. Anything else is answered 422 "Failed to deserialize" (live 2026-10-19, "7m"), so it is
362
+ * refused as INVALID_PARAMS before sending. "1M" buckets are 30 days, not calendar months
363
+ * (live: consecutive 1M opens 30 d apart); "1w" buckets open on Thursday 00:00 UTC.
364
+ */
365
+ export const HL_CANDLE_INTERVAL_MS = {
366
+ "1m": 60_000, "3m": 180_000, "5m": 300_000, "15m": 900_000, "30m": 1_800_000,
367
+ "1h": 3_600_000, "2h": 7_200_000, "4h": 14_400_000, "8h": 28_800_000, "12h": 43_200_000,
368
+ "1d": 86_400_000, "3d": 259_200_000, "1w": 604_800_000, "1M": 2_592_000_000,
369
+ };
370
+ /** Docs "Candle snapshot": "Only the most recent 5000 candles are available". */
371
+ export const HL_CANDLE_CAP = 5000;
372
+ /** Earliest candleSnapshot start (unix ms) for `interval` at `now`: now − 5000 × interval. Unknown interval → NaN. */
373
+ export function hlEarliestCandleStart(interval, now = Date.now()) {
374
+ return now - HL_CANDLE_CAP * (HL_CANDLE_INTERVAL_MS[interval] ?? Number.NaN);
375
+ }
376
+ /** recentTrades returns the last 10 trades, with no count parameter (live 2026-10-19). */
377
+ export const HL_RECENT_TRADES_CAP = 10;
378
+ /** l2Book returns at most 20 levels per side (docs l2Book; live 2026-10-19). */
379
+ export const HL_BOOK_LEVELS_CAP = 20;
380
+ /** Time-range info responses carry at most 500 elements (docs "Info endpoint" pagination note). */
381
+ const HL_INFO_PAGE = 500;
382
+ /** Largest `market funding --limit` (hourly rows ≈ 208 days) and the page guard of one read. */
383
+ export const HL_FUNDING_HISTORY_MAX_LIMIT = 5000;
384
+ const HL_FUNDING_HISTORY_MAX_PAGES = 20;
346
385
  /** A required field of an HL info row as a string; missing / empty → EXCHANGE_ERROR. */
347
386
  function hlField(row, key, endpoint) {
348
387
  const v = row[key];
@@ -362,6 +401,203 @@ function hlSide(v, endpoint) {
362
401
  return "sell";
363
402
  throw new PerpError("EXCHANGE_ERROR", `Hyperliquid ${endpoint} row with unknown side ${JSON.stringify(v)}`, { exchange: "hyperliquid" });
364
403
  }
404
+ /**
405
+ * Open positions of one clearinghouseState (info `clearinghouseState` {user[, dex]}): rows with
406
+ * szi 0 dropped. The ONE mapper of the keyed `getPositions` and the public-address read
407
+ * (`account positions --address`); `symbolOf` names the row (keyed: the adapter's display name,
408
+ * public: the plain coin — "BTC", HIP-3 "xyz:GOLD").
409
+ */
410
+ export function hlPositionRows(state, symbolOf) {
411
+ const positions = (state?.assetPositions ?? []);
412
+ return positions
413
+ .filter((p) => {
414
+ const pos = (p.position ?? p);
415
+ return parseFiniteVenueNumber(pos.szi, "position.szi", "hyperliquid") !== 0;
416
+ })
417
+ .map((p) => {
418
+ const pos = (p.position ?? p);
419
+ const szi = parseFiniteVenueNumber(pos.szi, "position.szi", "hyperliquid");
420
+ const positionValue = pos.positionValue !== undefined && pos.positionValue !== null
421
+ ? parseFiniteVenueNumber(pos.positionValue, "position.positionValue", "hyperliquid")
422
+ : 0;
423
+ return {
424
+ symbol: symbolOf(String(pos.coin ?? "")),
425
+ side: szi > 0 ? "long" : "short",
426
+ size: String(Math.abs(szi)),
427
+ entryPrice: String(pos.entryPx ?? "0"),
428
+ // clearinghouseState has no per-position mark: positionValue / |szi| is computed, so it is
429
+ // formatted (no "2727.2000000000003").
430
+ markPrice: szi !== 0 && positionValue !== 0
431
+ ? formatComputedPrice(positionValue / Math.abs(szi))
432
+ : String(pos.markPx ?? "0"),
433
+ liquidationPrice: pos.liquidationPx != null ? String(pos.liquidationPx) : null,
434
+ unrealizedPnl: String(pos.unrealizedPnl ?? "0"),
435
+ leverage: parseFiniteVenueNumber(pos.leverage?.value, "position.leverage.value", "hyperliquid", { defaultValue: 1 }),
436
+ };
437
+ });
438
+ }
439
+ /**
440
+ * info `userAbstraction` answer → mode. Per SSOT Rule #2: an unrecognized venue response throws —
441
+ * no silent default.
442
+ */
443
+ export function hlAbstractionModeOf(raw) {
444
+ if (raw === "unifiedAccount")
445
+ return "unified";
446
+ if (raw === "portfolioMargin")
447
+ return "portfolio";
448
+ if (raw === "disabled")
449
+ return "standard";
450
+ if (raw === "default")
451
+ return "default";
452
+ // "dexAbstraction" is HL's discontinued fourth mode, still returned for some users. It is
453
+ // NOT unified: "USDC balances default to perps balance, all other collateral defaults to
454
+ // spot balance" (docs trading/account-abstraction-modes). Handled explicitly in hlBalanceOf().
455
+ if (raw === "dexAbstraction")
456
+ return "dexAbstraction";
457
+ throw new PerpError("INVALID_PARAMS", `UNKNOWN_ACCOUNT_MODE: HL returned unrecognized userAbstraction mode: ${JSON.stringify(raw)}`, { remediation: "Check app.hyperliquid.xyz Settings → Account Mode" });
458
+ }
459
+ /**
460
+ * Portfolio-margin collateral rows: the venue returns an `ltv` on exactly the eligible collateral
461
+ * tokens (live 2026-10-26: USDC, HYPE, USDT0 carry it, memecoins do not; unified-mode rows never
462
+ * do), plus the dex's collateral token itself.
463
+ */
464
+ export function hlIsPortfolioCollateral(row, collateralToken) {
465
+ return Number(row.token) === collateralToken || Object.prototype.hasOwnProperty.call(row, "ltv");
466
+ }
467
+ /**
468
+ * USDC price of each spot token (index → price) from info `spotMetaAndAssetCtxs`: the mark of its
469
+ * USDC-quoted pair (quote token 0); several pairs for one token → the one with the larger 24h
470
+ * notional. USDC itself is 1. Malformed payload → EXCHANGE_ERROR (never a guessed price).
471
+ */
472
+ export function hlSpotUsdPricesOf(raw) {
473
+ const arr = raw;
474
+ const universe = Array.isArray(arr) ? arr[0]?.universe : undefined;
475
+ const ctxs = Array.isArray(arr) ? arr[1] : undefined;
476
+ if (!Array.isArray(universe) || !Array.isArray(ctxs)) {
477
+ throw new PerpError("EXCHANGE_ERROR", "Hyperliquid spotMetaAndAssetCtxs: expected [meta, ctxs]", { exchange: "hyperliquid" });
478
+ }
479
+ const best = new Map();
480
+ for (const pair of universe) {
481
+ const tokens = pair.tokens;
482
+ if (!Array.isArray(tokens) || tokens[1] !== 0 || typeof pair.index !== "number")
483
+ continue;
484
+ const ctx = ctxs[pair.index];
485
+ const price = Number(ctx?.markPx);
486
+ if (!Number.isFinite(price) || price <= 0)
487
+ continue;
488
+ const vol = Number(ctx?.dayNtlVlm);
489
+ const base = Number(tokens[0]);
490
+ const prev = best.get(base);
491
+ if (prev === undefined || (Number.isFinite(vol) ? vol : 0) > prev.vol)
492
+ best.set(base, { price, vol: Number.isFinite(vol) ? vol : 0 });
493
+ }
494
+ const out = new Map([[0, 1]]);
495
+ for (const [t, v] of best)
496
+ out.set(t, v.price);
497
+ return out;
498
+ }
499
+ export async function hlBalanceOf(state, mode, dex, reads) {
500
+ const s = state;
501
+ const notes = [];
502
+ const margin = (s?.marginSummary ?? {});
503
+ const cross = (s?.crossMarginSummary ?? {});
504
+ const marginUsed = parseFiniteVenueNumber(margin.totalMarginUsed ?? cross.totalMarginUsed, "marginSummary.totalMarginUsed", "hyperliquid");
505
+ // Sum unrealized PnL directly from positions (reliable for both main + dex accounts)
506
+ const positions = (s?.assetPositions ?? []);
507
+ let unrealizedPnl = 0;
508
+ for (const entry of positions) {
509
+ const pos = (entry.position ?? entry);
510
+ unrealizedPnl += parseFiniteVenueNumber(pos.unrealizedPnl, "position.unrealizedPnl", "hyperliquid");
511
+ }
512
+ let equity;
513
+ let available;
514
+ let portfolio = null;
515
+ // dexAbstraction (discontinued): USDC (token 0) lives in the NATIVE perp balance, any other
516
+ // collateral token in spot. Native perps → the perp state below. A HIP-3 dex with non-USDC
517
+ // collateral → its spot row. A USDC-collateral HIP-3 dex is refused: its dex state is not the
518
+ // balance ("Cross margin on HIP-3 DEXs does not behave intuitively for DEX abstraction users").
519
+ let spotIsTruth = mode === "unified" || mode === "portfolio";
520
+ if (mode === "dexAbstraction" && dex) {
521
+ if (await reads.collateralToken() === 0) {
522
+ throw new PerpError("NOT_IMPLEMENTED", `Hyperliquid account mode "dexAbstraction" (discontinued by the venue): the USDC balance of HIP-3 dex "${dex}" cannot be read reliably`, {
523
+ exchange: "hyperliquid",
524
+ remediation: "Switch the account mode first: perp -e hyperliquid wallet manage account-mode unified (or standard)",
525
+ });
526
+ }
527
+ spotIsTruth = true;
528
+ }
529
+ if (spotIsTruth) {
530
+ const { row, balances, token } = await reads.spotCollateralRow();
531
+ const coin = row === null ? `token#${token}` : String(row.coin);
532
+ const spotTotal = parseFiniteVenueNumber(row?.total, `spotBalance.${coin}.total`, "hyperliquid");
533
+ const spotHold = parseFiniteVenueNumber(row?.hold, `spotBalance.${coin}.hold`, "hyperliquid");
534
+ equity = spotTotal;
535
+ available = spotTotal - spotHold;
536
+ if (mode === "portfolio") {
537
+ const prices = await reads.spotUsdPrices();
538
+ const rows = [];
539
+ let value = 0;
540
+ for (const b of balances) {
541
+ if (!hlIsPortfolioCollateral(b, token))
542
+ continue;
543
+ const c = String(b.coin);
544
+ const total = parseFiniteVenueNumber(b.total, `spotBalance.${c}.total`, "hyperliquid");
545
+ if (total === 0)
546
+ continue; // an empty row adds nothing (USDT0 0 with a hold: the hold is in available)
547
+ const isCollateral = Number(b.token) === token;
548
+ let priceUsd;
549
+ if (isCollateral)
550
+ priceUsd = 1;
551
+ else {
552
+ const p = prices.get(Number(b.token));
553
+ if (p === undefined) {
554
+ throw new PerpError("PRICE_STALE", `Hyperliquid portfolio margin: no USDC spot price for collateral ${c} (token ${String(b.token)}) — equity unknown`, {
555
+ exchange: "hyperliquid", remediation: "Retry shortly; the spot market context read may have failed",
556
+ });
557
+ }
558
+ priceUsd = p;
559
+ }
560
+ const valueUsd = total * priceUsd;
561
+ value += valueUsd;
562
+ rows.push({
563
+ token: c, total: String(b.total), priceUsd: String(priceUsd), valueUsd: String(valueUsd),
564
+ ltv: typeof b.ltv === "string" ? b.ltv : null,
565
+ borrowed: typeof b.borrowed === "string" ? b.borrowed : null,
566
+ supplied: typeof b.supplied === "string" ? b.supplied : null,
567
+ });
568
+ }
569
+ equity = value;
570
+ const ratio = (await reads.spotCollateralRow()).portfolioMarginRatio;
571
+ portfolio = { collateral: rows, portfolioMarginRatio: typeof ratio === "string" ? ratio : null };
572
+ notes.push(`[hl] portfolio margin: equity = Σ collateral total × spot USDC price (${rows.map(r => `${r.token} ${r.total}${r.priceUsd !== "1" ? ` × ${r.priceUsd}` : ""}`).join(" + ")}); non-collateral spot tokens are listed in spot, vault equity is not included\n`);
573
+ }
574
+ }
575
+ else {
576
+ equity = parseFiniteVenueNumber(margin.accountValue ?? cross.accountValue, "marginSummary.accountValue", "hyperliquid");
577
+ available = parseFiniteVenueNumber(s?.withdrawable, "withdrawable", "hyperliquid");
578
+ if (mode === "default" && equity === 0) {
579
+ // Unset mode: do not assume unified. If the perp side reads 0 while spot holds
580
+ // the collateral token, say so instead of reporting an empty account silently.
581
+ const { row, token } = await reads.spotCollateralRow();
582
+ const spotTotal = parseFiniteVenueNumber(row?.total, `spotBalance.token#${token}.total`, "hyperliquid");
583
+ if (spotTotal > 0) {
584
+ notes.push(`[hl] account mode is unset ("default"): perp equity${dex ? ` on dex ${dex}` : ""} is 0 while spot holds ${spotTotal} ${String(row?.coin)}. ` +
585
+ `That spot balance is not perp collateral here. Either switch the account to unified ` +
586
+ `(perp wallet manage account-mode unified) or move it: perp -e hyperliquid trade spot-transfer to-perp <amount>\n`);
587
+ }
588
+ }
589
+ }
590
+ return {
591
+ balance: {
592
+ equity: String(equity),
593
+ available: String(available),
594
+ marginUsed: String(marginUsed),
595
+ unrealizedPnl: String(unrealizedPnl),
596
+ ...(portfolio ?? {}),
597
+ },
598
+ notes,
599
+ };
600
+ }
365
601
  export class HyperliquidAdapter {
366
602
  name = "hyperliquid";
367
603
  chain = "evm";
@@ -375,6 +611,8 @@ export class HyperliquidAdapter {
375
611
  * semantics for accounts that have never opted in. Codex v0.12.12 final QA #1.
376
612
  */
377
613
  isUnifiedAccount = false;
614
+ /** The account abstraction mode resolved in init() (null until then / without an address). */
615
+ accountMode = null;
378
616
  _address;
379
617
  _privateKey;
380
618
  _testnet;
@@ -400,6 +638,8 @@ export class HyperliquidAdapter {
400
638
  * configured. Set by createHyperliquid — the adapter never reads settings.
401
639
  */
402
640
  builderFee = null;
641
+ /** recentTrades' fixed tape size (no paging): `market trades` defaults to min(20, this). */
642
+ recentTradesCap = HL_RECENT_TRADES_CAP;
403
643
  constructor(privateKey, testnet = false) {
404
644
  // No SDK: /info reads go through _infoPost, /exchange actions are signed here (hl-signing.ts).
405
645
  this._address = "";
@@ -409,9 +649,39 @@ export class HyperliquidAdapter {
409
649
  /** Set the HIP-3 deployed perp dex to query/trade on. */
410
650
  setDex(dex) {
411
651
  this._dex = dex;
412
- // Rebuild asset map for the new dex
413
- this._assetMap.clear();
414
- this._assetMapReverse.clear();
652
+ // Fresh market state for the new dex — NEW objects, never .clear(): a map shared with another
653
+ // adapter (prototype copy) must not be emptied under it. Rebuilt lazily (_loadAssetMap).
654
+ this._assetMap = new Map();
655
+ this._assetMapReverse = new Map();
656
+ this._szDecimalsMap = new Map();
657
+ this._marginModeMap = new Map();
658
+ this._assetMapError = null;
659
+ this._marketsCache = null;
660
+ this._marketsCacheTime = 0;
661
+ this._delisted = new Set();
662
+ this._delistedAssets = new Set();
663
+ this._chsInFlight = null;
664
+ this._collateralTokenIdx = null;
665
+ }
666
+ /**
667
+ * An independent adapter for HIP-3 dex `dex` ("" = native perps) with this adapter's account
668
+ * configuration (key / signers / address / network / builder gate) and its OWN market state
669
+ * (asset maps, markets cache, delisted sets, in-flight reads). Fan-outs over dexes use this:
670
+ * Object.create(adapter) + setDex() shared the prototype's maps, so setDex cleared the native
671
+ * adapter's asset map and the dex copies served the native markets cache.
672
+ */
673
+ forDex(dex) {
674
+ const a = new HyperliquidAdapter(this._privateKey || undefined, this._testnet);
675
+ a._address = this._address;
676
+ a._evmSigner = this._evmSigner;
677
+ a._agentSigner = this._agentSigner;
678
+ a._agentMeta = this._agentMeta;
679
+ a._useNoAgent = this._useNoAgent;
680
+ a.isUnifiedAccount = this.isUnifiedAccount;
681
+ a.accountMode = this.accountMode;
682
+ a.builderFee = this.builderFee;
683
+ a.setDex(dex);
684
+ return a;
415
685
  }
416
686
  get dex() {
417
687
  return this._dex;
@@ -544,6 +814,7 @@ export class HyperliquidAdapter {
544
814
  // every caller that branches on the account mode.
545
815
  if (this._address) {
546
816
  const mode = await this._getAbstractionMode();
817
+ this.accountMode = mode;
547
818
  // dexAbstraction is not unified (USDC stays in the perp balance).
548
819
  this.isUnifiedAccount = mode === "unified" || mode === "portfolio";
549
820
  }
@@ -562,6 +833,7 @@ export class HyperliquidAdapter {
562
833
  /** Load asset index map — supports native and HIP-3 dex. */
563
834
  async _loadAssetMap() {
564
835
  this._assetMapError = null;
836
+ this._delistedAssets = new Set();
565
837
  try {
566
838
  if (this._dex) {
567
839
  // HIP-3 deployed dex: asset indices use global offset scheme
@@ -598,6 +870,10 @@ export class HyperliquidAdapter {
598
870
  this._assetMap.set(fullName, globalIdx);
599
871
  this._assetMap.set(baseName, globalIdx);
600
872
  this._assetMapReverse.set(globalIdx, fullName);
873
+ if (asset.isDelisted === true) {
874
+ this._delistedAssets.add(fullName);
875
+ this._delistedAssets.add(baseName);
876
+ }
601
877
  // Cache szDecimals for dex assets (critical for order sizing)
602
878
  if (asset.szDecimals !== undefined) {
603
879
  this._szDecimalsMap.set(fullName, asset.szDecimals);
@@ -620,6 +896,8 @@ export class HyperliquidAdapter {
620
896
  const sym = `${asset.name}-PERP`;
621
897
  this._assetMap.set(sym, idx);
622
898
  this._assetMapReverse.set(idx, sym);
899
+ if (asset.isDelisted === true)
900
+ this._delistedAssets.add(sym);
623
901
  if (asset.szDecimals !== undefined) {
624
902
  this._szDecimalsMap.set(sym, asset.szDecimals);
625
903
  }
@@ -653,7 +931,7 @@ export class HyperliquidAdapter {
653
931
  // A dex-prefixed symbol must name THIS adapter's dex — never rewritten onto another dex.
654
932
  const [prefix, base] = sym.split(":");
655
933
  if (prefix.toLowerCase() !== this._dex.toLowerCase()) {
656
- throw new PerpError("SYMBOL_NOT_FOUND", `Hyperliquid symbol ${symbol} is on dex "${prefix.toLowerCase()}", not ${this._dex ? `this adapter's dex "${this._dex}"` : "the native perp dex"}`, {
934
+ throw new PerpError("SYMBOL_NOT_FOUND", `Hyperliquid symbol ${prefix.toLowerCase()}:${base} is on dex "${prefix.toLowerCase()}", not ${this._dex ? `this adapter's dex "${this._dex}"` : "the native perp dex"}`, {
657
935
  exchange: "hyperliquid", remediation: `Select the dex: perp -e hyperliquid --dex ${prefix.toLowerCase()} ...`,
658
936
  });
659
937
  }
@@ -726,7 +1004,14 @@ export class HyperliquidAdapter {
726
1004
  }
727
1005
  return idx;
728
1006
  }
729
- throw new PerpError("SYMBOL_NOT_FOUND", `Unknown asset: ${symbol}`, { exchange: "hyperliquid" });
1007
+ throw this._unknownSymbol(symbol, `Unknown asset: ${symbol}`);
1008
+ }
1009
+ /** SYMBOL_NOT_FOUND for a coin the venue does not list (on this adapter's dex), with the listing command. */
1010
+ _unknownSymbol(symbol, message) {
1011
+ return new PerpError("SYMBOL_NOT_FOUND", message ?? `Hyperliquid has no market ${symbol.toUpperCase()}${this._dex ? ` on dex "${this._dex}"` : ""}`, {
1012
+ exchange: "hyperliquid",
1013
+ remediation: `List markets: perp -e hyperliquid${this._dex ? ` --dex ${this._dex}` : ""} market list`,
1014
+ });
730
1015
  }
731
1016
  async getMarkets() {
732
1017
  if (this._marketsCache && Date.now() - this._marketsCacheTime < HyperliquidAdapter.CACHE_TTL)
@@ -785,32 +1070,109 @@ export class HyperliquidAdapter {
785
1070
  this._assetMapReverse.set(i, sym);
786
1071
  });
787
1072
  }
788
- const result = universe.map((asset, i) => {
1073
+ // Delisted assets (meta `isDelisted: true`; live 2026-10-14: 56 of 234 native perps, all with
1074
+ // funding / dayNtlVlm / openInterest "0.0") are not markets: excluded from the listing and
1075
+ // everything built on it. They stay in the asset map (_loadAssetMap), so an explicit symbol
1076
+ // still resolves for orders / position reads.
1077
+ const delisted = new Set();
1078
+ const result = universe.flatMap((asset, i) => {
1079
+ if (asset.isDelisted === true) {
1080
+ delisted.add(String(asset.name));
1081
+ return [];
1082
+ }
789
1083
  const ctx = (ctxs[i] ?? {});
790
1084
  const sym = String(asset.name);
791
1085
  const szDec = this._szDecimalsMap.get(sym) ?? (asset.szDecimals !== undefined ? Number(asset.szDecimals) : null);
792
- return {
793
- symbol: sym,
794
- markPrice: String(ctx.markPx ?? mids[sym] ?? "0"),
795
- indexPrice: ctx.oraclePx != null ? String(ctx.oraclePx) : null,
796
- fundingRate: ctx.funding != null ? String(ctx.funding) : null,
797
- volume24h: ctx.dayNtlVlm != null ? String(ctx.dayNtlVlm) : null,
798
- openInterest: ctx.openInterest != null ? String(ctx.openInterest) : null,
799
- maxLeverage: Number(asset.maxLeverage ?? 50),
800
- sizeDecimals: szDec,
801
- // HL has no separate lot-size field; the step is implied by szDecimals.
802
- stepSize: null,
803
- // HL funding is uniform (venue definition), not per-market.
804
- fundingHours: null,
805
- marginTiers: tierMap.get(Number(asset.marginTableId)) ?? null,
806
- };
1086
+ return [{
1087
+ // One symbol shape on every venue: the plain asset ("BTC"; HIP-3 "km:US500"), the
1088
+ // venue's wire coin in venueSymbol. The internal "<COIN>-PERP" key is not shown.
1089
+ symbol: sym.replace(/-PERP$/, ""),
1090
+ // The coin exactly as the venue names it on the wire (native "kPEPE", HIP-3 "xyz:TSLA" —
1091
+ // lower-case dex prefix); _venueCoin upper-cases its input, so it is not used here.
1092
+ venueSymbol: sym.replace(/-PERP$/, ""),
1093
+ markPrice: String(ctx.markPx ?? mids[sym] ?? "0"),
1094
+ indexPrice: ctx.oraclePx != null ? String(ctx.oraclePx) : null,
1095
+ fundingRate: ctx.funding != null ? String(ctx.funding) : null,
1096
+ // metaAndAssetCtxs `funding` = the running hour's rate (WS allDexsAssetCtxs: accruing).
1097
+ fundingRateKind: ctx.funding != null ? "accruing" : null,
1098
+ volume24h: ctx.dayNtlVlm != null ? String(ctx.dayNtlVlm) : null,
1099
+ openInterest: ctx.openInterest != null ? String(ctx.openInterest) : null,
1100
+ maxLeverage: Number(asset.maxLeverage ?? 50),
1101
+ sizeDecimals: szDec,
1102
+ // HL has no separate lot-size field; the step is implied by szDecimals (10^-szDecimals,
1103
+ // the same value the keyless MCP get_markets path states).
1104
+ stepSize: szDec !== null && Number.isInteger(szDec) && szDec >= 0 ? (szDec === 0 ? "1" : `0.${"0".repeat(szDec - 1)}1`) : null,
1105
+ // HL funding is uniform: a fraction per 1 h (venue fundingRateBasis), core and HIP-3.
1106
+ fundingHours: 1,
1107
+ marginTiers: hlMarginTiersOf(asset.marginTableId, tierMap),
1108
+ }];
807
1109
  });
1110
+ this._delisted = delisted;
808
1111
  this._marketsCache = result;
809
1112
  this._marketsCacheTime = Date.now();
810
1113
  return result;
811
1114
  }
812
- async getOrderbook(symbol) {
1115
+ /** Display names of the delisted assets seen by the last getMarkets() (meta isDelisted). */
1116
+ _delisted = new Set();
1117
+ /** Asset-map keys of the delisted assets in `meta` (_loadAssetMap): "MATIC-PERP"; HIP-3 "km:US500" and "US500". */
1118
+ _delistedAssets = new Set();
1119
+ /**
1120
+ * Why `symbol` is not in getMarkets(): it is delisted (message), or null when it is not a
1121
+ * delisted asset. Read after getMarkets() (or once the asset map is loaded). `market info` uses
1122
+ * it to say "delisted" rather than "not found" — the asset map still resolves it for orders /
1123
+ * positions / history. A HIP-3 bare name ("US500" on --dex km) is matched by its venue coin.
1124
+ */
1125
+ delistedMessage(symbol) {
1126
+ const resolved = this.resolveSymbol(symbol.toUpperCase());
1127
+ const coin = this._venueCoin(symbol).replace(/^([^:]+):/, (_m, dex) => `${dex.toLowerCase()}:`); // "KM:US500" → "km:US500"
1128
+ const isDelisted = this._delisted.has(resolved) || this._delisted.has(coin) || this._delistedAssets.has(resolved);
1129
+ if (!isDelisted)
1130
+ return null;
1131
+ return `${this._dex ? coin : resolved.replace(/-PERP$/, "")} is delisted on Hyperliquid (positions / history still readable)`;
1132
+ }
1133
+ /** SYMBOL_NOT_FOUND (details.reason "delisted") for a delisted `symbol`, with a dex-aware remediation; null when listed. */
1134
+ delistedError(symbol) {
1135
+ const msg = this.delistedMessage(symbol);
1136
+ if (msg === null)
1137
+ return null;
1138
+ return new PerpError("SYMBOL_NOT_FOUND", msg, {
1139
+ exchange: "hyperliquid",
1140
+ reason: "delisted",
1141
+ remediation: `List live markets: perp -e hyperliquid${this._dex ? ` --dex ${this._dex}` : ""} market list`,
1142
+ });
1143
+ }
1144
+ /**
1145
+ * Per-symbol MARKET-DATA reads (trades, funding, book, klines): the symbol must resolve
1146
+ * (SYMBOL_NOT_FOUND) and must not be delisted — HL keeps serving a delisted coin's last tape
1147
+ * (MATIC: trades from 2024-09-09) and zero-rate funding rows stamped every hour (live
1148
+ * 2026-10-19), which read as a live market. Orders / positions / history do not call this.
1149
+ */
1150
+ async _listedMarket(symbol) {
1151
+ await this.getAssetIndex(symbol);
1152
+ const err = this.delistedError(symbol);
1153
+ if (err !== null)
1154
+ throw err;
1155
+ }
1156
+ /**
1157
+ * Public order-book read (`market book`, bots, MCP): l2Book serves at most HL_BOOK_LEVELS_CAP
1158
+ * (20) levels per side and cannot be paged, so a `depth` above it is INVALID_PARAMS naming the
1159
+ * cap. Refuses delisted markets (_listedMarket). Order paths use _l2Book (no delisted gate):
1160
+ * a reduce-only close of a delisted asset must still get its reference price.
1161
+ */
1162
+ async getOrderbook(symbol, depth) {
1163
+ if (depth !== undefined && !(Number.isInteger(depth) && depth > 0 && depth <= HL_BOOK_LEVELS_CAP)) {
1164
+ throw new PerpError("INVALID_PARAMS", `Hyperliquid order book: depth must be an integer 1..${HL_BOOK_LEVELS_CAP} (got ${depth}) — l2Book serves at most ${HL_BOOK_LEVELS_CAP} levels per side`, {
1165
+ exchange: "hyperliquid", remediation: `Use -d ${HL_BOOK_LEVELS_CAP} or less`,
1166
+ });
1167
+ }
1168
+ await this._listedMarket(symbol);
1169
+ return this._l2Book(symbol);
1170
+ }
1171
+ /** l2Book of `symbol` (native or this adapter's dex), no listing gate. Unlisted coin → SYMBOL_NOT_FOUND. */
1172
+ async _l2Book(symbol) {
813
1173
  let levels;
1174
+ // l2Book answers `null` for a coin the venue does not list (live 2026-10-14, coin "NOPE"):
1175
+ // SYMBOL_NOT_FOUND, never an empty book (which reads as "no liquidity").
814
1176
  if (this._dex) {
815
1177
  // HIP-3 dex: full dex-prefixed coin (e.g. "US500" → "km:US500") + dex param
816
1178
  const book = await this._infoPost({
@@ -818,11 +1180,15 @@ export class HyperliquidAdapter {
818
1180
  coin: this._venueCoin(symbol),
819
1181
  dex: this._dex,
820
1182
  });
821
- levels = book?.levels ?? [[], []];
1183
+ if (book === null)
1184
+ throw this._unknownSymbol(symbol);
1185
+ levels = book.levels ?? [[], []];
822
1186
  }
823
1187
  else {
824
1188
  const book = await this._infoPost({ type: "l2Book", coin: this._venueCoin(symbol) });
825
- levels = book?.levels ?? [[], []];
1189
+ if (book === null)
1190
+ throw this._unknownSymbol(symbol);
1191
+ levels = book.levels ?? [[], []];
826
1192
  }
827
1193
  return {
828
1194
  bids: (levels[0] ?? []).map((l) => [
@@ -851,7 +1217,7 @@ export class HyperliquidAdapter {
851
1217
  }
852
1218
  ensureAddress() {
853
1219
  if (!this._address) {
854
- throw new Error("No private key configured — account data unavailable. Run: perp setup");
1220
+ throw noAccountKeyError(this.name);
855
1221
  }
856
1222
  }
857
1223
  /** The in-flight clearinghouseState read: parallel callers (getBalance + getPositions) share one request. */
@@ -894,23 +1260,7 @@ export class HyperliquidAdapter {
894
1260
  async _getAbstractionMode(opts = {}) {
895
1261
  this.ensureAddress();
896
1262
  const key = `${hlAbstractionCachePrefix(this._network)}${this._address.toLowerCase()}`;
897
- return (opts.fresh === true ? fetchAndCache : withCache)(key, HL_ABSTRACTION_TTL_MS, async () => {
898
- const raw = (await this._infoPost({ type: "userAbstraction", user: this._address }));
899
- if (raw === "unifiedAccount")
900
- return "unified";
901
- if (raw === "portfolioMargin")
902
- return "portfolio";
903
- if (raw === "disabled")
904
- return "standard";
905
- if (raw === "default")
906
- return "default";
907
- // "dexAbstraction" is HL's discontinued fourth mode, still returned for some users. It is
908
- // NOT unified: "USDC balances default to perps balance, all other collateral defaults to
909
- // spot balance" (docs trading/account-abstraction-modes). Handled explicitly in getBalance().
910
- if (raw === "dexAbstraction")
911
- return "dexAbstraction";
912
- throw new PerpError("INVALID_PARAMS", `UNKNOWN_ACCOUNT_MODE: HL returned unrecognized userAbstraction mode: ${JSON.stringify(raw)}`, { remediation: "Check app.hyperliquid.xyz Settings → Account Mode" });
913
- });
1263
+ return (opts.fresh === true ? fetchAndCache : withCache)(key, HL_ABSTRACTION_TTL_MS, async () => hlAbstractionModeOf(await this._infoPost({ type: "userAbstraction", user: this._address })));
914
1264
  }
915
1265
  /** Collateral token index of this adapter's perp dex (info `meta{dex}.collateralToken`; 0 = USDC on native perps). */
916
1266
  _collateralTokenIdx = null;
@@ -941,127 +1291,22 @@ export class HyperliquidAdapter {
941
1291
  const [spotState, token] = await Promise.all([this._getSpotClearinghouseState(), this._getCollateralToken()]);
942
1292
  const balances = (spotState?.balances ?? []);
943
1293
  const row = balances.find((b) => Number(b.token) === token) ?? null;
944
- return { row, balances, token };
1294
+ return { row, balances, token, portfolioMarginRatio: spotState?.portfolioMarginRatio };
945
1295
  }
946
1296
  async getBalance() {
947
1297
  const state = await this._getClearinghouseState();
948
- const s = state;
949
- const margin = (s?.marginSummary ?? {});
950
- const cross = (s?.crossMarginSummary ?? {});
951
- const marginUsed = parseFiniteVenueNumber(margin.totalMarginUsed ?? cross.totalMarginUsed, "marginSummary.totalMarginUsed", "hyperliquid");
952
- // Sum unrealized PnL directly from positions (reliable for both main + dex accounts)
953
- const positions = (s?.assetPositions ?? []);
954
- let unrealizedPnl = 0;
955
- for (const entry of positions) {
956
- const pos = (entry.position ?? entry);
957
- unrealizedPnl += parseFiniteVenueNumber(pos.unrealizedPnl, "position.unrealizedPnl", "hyperliquid");
958
- }
959
- let equity;
960
- let available;
961
- // Branch by the user's account abstraction mode (Rule #2: no silent fallback).
962
- // - "unified" / "portfolio": per HL docs, API users see all balances and holds
963
- // in the spot clearinghouse state; the dex's collateral token row (by token
964
- // index from meta{dex}.collateralToken) is the equity. Adding perp margin
965
- // would double-count.
966
- // - "standard" / "default": perp clearinghouse (per dex) is the collateral source.
967
- //
968
- // Portfolio-margin mode: eligible collateral includes HYPE, BTC, USDT besides
969
- // USDC. Only the collateral token is counted; a stderr note lists the rest.
970
1298
  const mode = await this._getAbstractionMode();
971
- // dexAbstraction (discontinued): USDC (token 0) lives in the NATIVE perp balance, any other
972
- // collateral token in spot. Native perps → the perp state below. A HIP-3 dex with non-USDC
973
- // collateral → its spot row. A USDC-collateral HIP-3 dex is refused: its dex state is not the
974
- // balance ("Cross margin on HIP-3 DEXs does not behave intuitively for DEX abstraction users").
975
- let spotIsTruth = mode === "unified" || mode === "portfolio";
976
- if (mode === "dexAbstraction" && this._dex) {
977
- if (await this._getCollateralToken() === 0) {
978
- throw new PerpError("NOT_IMPLEMENTED", `Hyperliquid account mode "dexAbstraction" (discontinued by the venue): the USDC balance of HIP-3 dex "${this._dex}" cannot be read reliably`, {
979
- exchange: "hyperliquid",
980
- remediation: "Switch the account mode first: perp -e hyperliquid wallet manage account-mode unified (or standard)",
981
- });
982
- }
983
- spotIsTruth = true;
984
- }
985
- if (spotIsTruth) {
986
- const { row, balances, token } = await this._spotCollateralRow();
987
- const coin = row === null ? `token#${token}` : String(row.coin);
988
- const spotTotal = parseFiniteVenueNumber(row?.total, `spotBalance.${coin}.total`, "hyperliquid");
989
- const spotHold = parseFiniteVenueNumber(row?.hold, `spotBalance.${coin}.hold`, "hyperliquid");
990
- equity = spotTotal;
991
- available = spotTotal - spotHold;
992
- if (mode === "portfolio") {
993
- // Eligible collateral per trading/account-abstraction-modes.md ("eligible
994
- // assets, which are currently HYPE, BTC, USDC, USDT").
995
- const PORTFOLIO_COLLATERAL = ["HYPE", "BTC", "USDT"];
996
- const others = balances.filter((b) => {
997
- if (Number(b.token) === token)
998
- return false;
999
- const c = String(b.coin);
1000
- // Reject NaN/empty here too — venue payload corruption shouldn't
1001
- // silently filter a token in or out.
1002
- const total = parseFiniteVenueNumber(b.total, `spotBalance.${c}.total`, "hyperliquid");
1003
- if (total <= 0)
1004
- return false;
1005
- return PORTFOLIO_COLLATERAL.some((p) => c === p || c.startsWith(`${p}-`));
1006
- });
1007
- if (others.length > 0) {
1008
- const summary = others.map((b) => `${b.coin}=${b.total}`).join(", ");
1009
- process.stderr.write(`[hl] portfolio mode: non-${coin} collateral present but not counted in equity (${summary}). ` +
1010
- `Equity reflects ${coin} only; full multi-asset accounting pending.\n`);
1011
- }
1012
- }
1013
- }
1014
- else {
1015
- equity = parseFiniteVenueNumber(margin.accountValue ?? cross.accountValue, "marginSummary.accountValue", "hyperliquid");
1016
- available = parseFiniteVenueNumber(s?.withdrawable, "withdrawable", "hyperliquid");
1017
- if (mode === "default" && equity === 0) {
1018
- // Unset mode: do not assume unified. If the perp side reads 0 while spot holds
1019
- // the collateral token, say so instead of reporting an empty account silently.
1020
- const { row, token } = await this._spotCollateralRow();
1021
- const spotTotal = parseFiniteVenueNumber(row?.total, `spotBalance.token#${token}.total`, "hyperliquid");
1022
- if (spotTotal > 0) {
1023
- process.stderr.write(`[hl] account mode is unset ("default"): perp equity${this._dex ? ` on dex ${this._dex}` : ""} is 0 while spot holds ${spotTotal} ${String(row?.coin)}. ` +
1024
- `That spot balance is not perp collateral here. Either switch the account to unified ` +
1025
- `(perp wallet manage account-mode unified) or move it: perp -e hyperliquid trade spot-transfer to-perp <amount>\n`);
1026
- }
1027
- }
1028
- }
1029
- return {
1030
- equity: String(equity),
1031
- available: String(available),
1032
- marginUsed: String(marginUsed),
1033
- unrealizedPnl: String(unrealizedPnl),
1034
- };
1299
+ const { balance, notes } = await hlBalanceOf(state, mode, this._dex, {
1300
+ spotCollateralRow: () => this._spotCollateralRow(),
1301
+ collateralToken: () => this._getCollateralToken(),
1302
+ spotUsdPrices: async () => hlSpotUsdPricesOf(await this._infoPost({ type: "spotMetaAndAssetCtxs" })),
1303
+ });
1304
+ for (const n of notes)
1305
+ process.stderr.write(n);
1306
+ return balance;
1035
1307
  }
1036
1308
  async getPositions() {
1037
- const state = await this._getClearinghouseState();
1038
- const positions = (state?.assetPositions ?? []);
1039
- return positions
1040
- .filter((p) => {
1041
- const pos = (p.position ?? p);
1042
- return parseFiniteVenueNumber(pos.szi, "position.szi", "hyperliquid") !== 0;
1043
- })
1044
- .map((p) => {
1045
- const pos = (p.position ?? p);
1046
- const szi = parseFiniteVenueNumber(pos.szi, "position.szi", "hyperliquid");
1047
- const positionValue = pos.positionValue !== undefined && pos.positionValue !== null
1048
- ? parseFiniteVenueNumber(pos.positionValue, "position.positionValue", "hyperliquid")
1049
- : 0;
1050
- return {
1051
- symbol: this._displaySymbol(String(pos.coin ?? "")),
1052
- side: szi > 0 ? "long" : "short",
1053
- size: String(Math.abs(szi)),
1054
- entryPrice: String(pos.entryPx ?? "0"),
1055
- // clearinghouseState has no per-position mark: positionValue / |szi| is computed, so it is
1056
- // formatted (no "2727.2000000000003").
1057
- markPrice: szi !== 0 && positionValue !== 0
1058
- ? formatComputedPrice(positionValue / Math.abs(szi))
1059
- : String(pos.markPx ?? "0"),
1060
- liquidationPrice: pos.liquidationPx != null ? String(pos.liquidationPx) : null,
1061
- unrealizedPnl: String(pos.unrealizedPnl ?? "0"),
1062
- leverage: parseFiniteVenueNumber(pos.leverage?.value, "position.leverage.value", "hyperliquid", { defaultValue: 1 }),
1063
- };
1064
- });
1309
+ return hlPositionRows(await this._getClearinghouseState(), (coin) => this._displaySymbol(coin));
1065
1310
  }
1066
1311
  /**
1067
1312
  * The leverage this account applies to `symbol`, position or not — info `activeAssetData`
@@ -1234,11 +1479,11 @@ export class HyperliquidAdapter {
1234
1479
  }
1235
1480
  }
1236
1481
  /**
1237
- * Market order = slippage-bounded IOC limit (design §13.2, §13.10 #1/#5) for
1482
+ * Market order = slippage-bounded IOC limit for
1238
1483
  * native perps and HIP-3 dexes alike. Replaces the SDK's marketOpen/marketClose
1239
1484
  * (hidden built-in slippage) and the former HIP-3 5% constant.
1240
1485
  *
1241
- * Reference price (the single declared source, §13.10 #5): the l2Book mid
1486
+ * Reference price (the single declared source): the l2Book mid
1242
1487
  * (best bid + best ask) / 2 of this coin — see referencePrice(). No runtime
1243
1488
  * substitute (mark, allMids, last trade): a missing side → PRICE_STALE before
1244
1489
  * anything is sent. Protection = reference ± opts.slippage.bps, aligned toward
@@ -1287,10 +1532,11 @@ export class HyperliquidAdapter {
1287
1532
  /**
1288
1533
  * The single declared reference price for market-type protection prices:
1289
1534
  * l2Book mid = (best bid + best ask) / 2. Works for native perps and HIP-3
1290
- * (getOrderbook passes the dex). Either side missing / non-positive → PRICE_STALE.
1535
+ * (_l2Book passes the dex). Either side missing / non-positive → PRICE_STALE. Not gated on
1536
+ * delisting: a reduce-only market close of a delisted asset is sent (the venue decides).
1291
1537
  */
1292
1538
  async referencePrice(symbol) {
1293
- const book = await this.getOrderbook(symbol);
1539
+ const book = await this._l2Book(symbol);
1294
1540
  const bid = Number(book.bids[0]?.[0]);
1295
1541
  const ask = Number(book.asks[0]?.[0]);
1296
1542
  if (!(bid > 0) || !(ask > 0) || !Number.isFinite(bid) || !Number.isFinite(ask)) {
@@ -1531,7 +1777,7 @@ export class HyperliquidAdapter {
1531
1777
  return hlAlignPriceToGrid(price, this.getSzDecimals(symbol), 6, side === "buy" ? "down" : "up");
1532
1778
  }
1533
1779
  async limitOrder(symbol, side, price, size, opts) {
1534
- // TIF aliases (§13.3): GTC/IOC/ALO or native spellings; FOK → CAPABILITY_UNSUPPORTED.
1780
+ // TIF aliases: GTC/IOC/ALO or native spellings; FOK → CAPABILITY_UNSUPPORTED.
1535
1781
  let action;
1536
1782
  const cloid = opts?.clientOrderId === undefined ? null : HyperliquidAdapter.requireCloid(opts.clientOrderId);
1537
1783
  try { // pre-send (see marketOrder)
@@ -1680,7 +1926,7 @@ export class HyperliquidAdapter {
1680
1926
  }
1681
1927
  // ── Interface methods ──
1682
1928
  /**
1683
- * Amend (design §13.4, §13.10 #10) via HL native batchModify. `size` = the NEW
1929
+ * Amend via HL native batchModify. `size` = the NEW
1684
1930
  * open (unfilled) size — native-modify semantics, not the original total. The
1685
1931
  * order's own tif (Gtc/Alo) and reduceOnly are read from frontendOpenOrders
1686
1932
  * and preserved; nothing is hardcoded.
@@ -1769,33 +2015,84 @@ export class HyperliquidAdapter {
1769
2015
  reduceOnly: opts?.reduceOnly ?? true,
1770
2016
  });
1771
2017
  }
1772
- async getRecentTrades(symbol, limit = 20) {
2018
+ /**
2019
+ * recentTrades: the venue's last HL_RECENT_TRADES_CAP (10) trades — no count parameter, no
2020
+ * paging. `limit` undefined = the venue max (10); an explicit `limit` above 10 is INVALID_PARAMS
2021
+ * naming the cap (never fewer rows than asked, silently). Row `symbol` is the raw venue coin
2022
+ * ("BTC", "km:US500"), as in the WS codec — not the "BTC-PERP" display name of `market list`.
2023
+ */
2024
+ async getRecentTrades(symbol, limit) {
2025
+ const n = limit ?? HL_RECENT_TRADES_CAP;
2026
+ if (!(Number.isInteger(n) && n > 0)) {
2027
+ throw new PerpError("INVALID_PARAMS", `Hyperliquid trades: limit must be a positive integer (got ${limit})`, { exchange: "hyperliquid" });
2028
+ }
2029
+ if (n > HL_RECENT_TRADES_CAP) {
2030
+ throw new PerpError("INVALID_PARAMS", `Hyperliquid trades: limit ${n} exceeds the venue cap of ${HL_RECENT_TRADES_CAP} (recentTrades serves only the last ${HL_RECENT_TRADES_CAP} trades, no paging)`, {
2031
+ exchange: "hyperliquid", remediation: `Use --limit ${HL_RECENT_TRADES_CAP} or less`,
2032
+ });
2033
+ }
2034
+ // recentTrades answers HTTP 500 "null" for an unlisted coin (live 2026-10-14), which would read
2035
+ // as a retryable venue error: resolve the coin against the asset map first (SYMBOL_NOT_FOUND).
2036
+ await this._listedMarket(symbol);
1773
2037
  const trades = await this._infoPost({ type: "recentTrades", coin: this._venueCoin(symbol) });
1774
- return (trades ?? [])
1775
- .slice(0, limit)
2038
+ if (trades === null)
2039
+ throw this._unknownSymbol(symbol);
2040
+ return trades
2041
+ .slice(0, n)
1776
2042
  .map((t) => ({
1777
2043
  time: Number(t.time ?? 0),
1778
2044
  symbol: String(t.coin ?? symbol.toUpperCase()),
1779
2045
  side: String(t.side) === "B" ? "buy" : "sell",
1780
2046
  price: String(t.px ?? "0"),
1781
2047
  size: String(t.sz ?? ""),
1782
- fee: "0",
2048
+ fee: null, // the public tape carries no fee
1783
2049
  }));
1784
2050
  }
2051
+ /**
2052
+ * candleSnapshot. Only the most recent HL_CANDLE_CAP (5000) candles of an interval exist (docs)
2053
+ * and older ones cannot be paged to: a `startTime` before hlEarliestCandleStart() is refused
2054
+ * up front (INVALID_PARAMS naming the earliest allowed start) — never a silently shortened
2055
+ * range. `closed` (additive) is false on the in-progress candle (close time `T`, or open +
2056
+ * interval when `T` is absent, still in the future). "1M" = 30-day buckets; "1w" opens
2057
+ * Thursday 00:00 UTC.
2058
+ */
1785
2059
  async getKlines(symbol, interval, startTime, endTime) {
2060
+ const ivMs = HL_CANDLE_INTERVAL_MS[interval];
2061
+ if (ivMs === undefined) {
2062
+ throw new PerpError("INVALID_PARAMS", `Hyperliquid has no ${JSON.stringify(interval)} candles`, {
2063
+ exchange: "hyperliquid", remediation: `Use one of: ${Object.keys(HL_CANDLE_INTERVAL_MS).join(", ")}`,
2064
+ });
2065
+ }
2066
+ const now = Date.now();
2067
+ const earliest = hlEarliestCandleStart(interval, now);
2068
+ if (startTime < earliest) {
2069
+ throw new PerpError("INVALID_PARAMS", `Hyperliquid serves only the most recent ${HL_CANDLE_CAP} ${interval} candles: the earliest allowed start is ${new Date(earliest).toISOString()} (${earliest}); requested ${new Date(startTime).toISOString()}`, {
2070
+ exchange: "hyperliquid", remediation: `Use --start ${earliest} or later, or a longer interval`,
2071
+ });
2072
+ }
2073
+ await this._listedMarket(symbol); // unlisted coin: SYMBOL_NOT_FOUND (candleSnapshot answers HTTP 500 "null")
1786
2074
  const candles = await this._infoPost({
1787
2075
  type: "candleSnapshot",
1788
2076
  req: { coin: this._venueCoin(symbol), interval, startTime, endTime },
1789
2077
  });
1790
- return (candles ?? []).map((c) => ({
1791
- time: Number(c.t ?? 0),
1792
- open: String(c.o ?? "0"),
1793
- high: String(c.h ?? "0"),
1794
- low: String(c.l ?? "0"),
1795
- close: String(c.c ?? "0"),
1796
- volume: String(c.v ?? ""),
1797
- trades: Number(c.n ?? 0),
1798
- }));
2078
+ return (candles ?? []).map((c) => {
2079
+ assertCandleInterval(this.name, interval, c.i);
2080
+ const time = Number(c.t ?? 0);
2081
+ const closeAt = Number.isFinite(Number(c.T)) && c.T !== undefined && c.T !== null ? Number(c.T) : time + ivMs;
2082
+ return {
2083
+ time,
2084
+ open: String(c.o ?? "0"),
2085
+ high: String(c.h ?? "0"),
2086
+ low: String(c.l ?? "0"),
2087
+ close: String(c.c ?? "0"),
2088
+ // absent / non-numeric → null (never "")
2089
+ volume: typeof c.v === "string" && c.v.trim() !== "" && Number.isFinite(Number(c.v)) ? c.v
2090
+ : typeof c.v === "number" && Number.isFinite(c.v) ? String(c.v) : null,
2091
+ quoteVolume: null, // candleSnapshot carries base volume only
2092
+ trades: Number(c.n ?? 0),
2093
+ closed: closeAt < now,
2094
+ };
2095
+ });
1799
2096
  }
1800
2097
  /**
1801
2098
  * Order history: POST /info {type:"historicalOrders"} ("Retrieve a user's historical orders",
@@ -1942,15 +2239,59 @@ export class HyperliquidAdapter {
1942
2239
  return coin.startsWith(`${this._dex}:`);
1943
2240
  return !/[:@#/]/.test(coin);
1944
2241
  }
2242
+ /**
2243
+ * The last `limit` hourly funding settlements. HL funding is hourly, so the window starts
2244
+ * `limit + 1` hours back (one spare hour for a settlement not yet published), and fundingHistory
2245
+ * pages are read forward from the last row's time (docs: at most 500 elements per response;
2246
+ * "use the last returned timestamp as the next startTime") — deduped by time, oldest first.
2247
+ */
1945
2248
  async getFundingHistory(symbol, limit = 10) {
1946
- const now = Date.now();
1947
- const history = await this._infoPost({ type: "fundingHistory", coin: this._venueCoin(symbol), startTime: now - 24 * 60 * 60 * 1000 });
1948
- return (history ?? []).slice(-limit).map((h) => ({
2249
+ if (!(Number.isInteger(limit) && limit > 0)) {
2250
+ throw new PerpError("INVALID_PARAMS", `Hyperliquid funding history: limit must be a positive integer (got ${limit})`, { exchange: "hyperliquid" });
2251
+ }
2252
+ if (limit > HL_FUNDING_HISTORY_MAX_LIMIT) {
2253
+ throw new PerpError("INVALID_PARAMS", `Hyperliquid funding history: limit ${limit} exceeds ${HL_FUNDING_HISTORY_MAX_LIMIT} hourly rows`, { exchange: "hyperliquid", remediation: `Use --limit ${HL_FUNDING_HISTORY_MAX_LIMIT} or less` });
2254
+ }
2255
+ await this._listedMarket(symbol); // unlisted coin: SYMBOL_NOT_FOUND (fundingHistory answers HTTP 500)
2256
+ const history = await this._fundingHistorySince(this._venueCoin(symbol), Date.now() - (limit + 1) * 60 * 60 * 1000);
2257
+ return history.slice(-limit).map((h) => ({
1949
2258
  time: Number(h.time ?? 0),
1950
2259
  rate: venueRateField(h.fundingRate, "fundingHistory fundingRate", "hyperliquid"),
1951
2260
  price: null,
2261
+ intervalHours: 1,
2262
+ rateKind: "lastSettled",
1952
2263
  }));
1953
2264
  }
2265
+ /** Every fundingHistory row of `coin` since `startTime`, paged forward (500 per response), deduped by time, oldest first. */
2266
+ async _fundingHistorySince(coin, startTime) {
2267
+ const byTime = new Map();
2268
+ let from = startTime;
2269
+ for (let page = 0;; page++) {
2270
+ if (page >= HL_FUNDING_HISTORY_MAX_PAGES) {
2271
+ throw new PerpError("EXCHANGE_ERROR", `Hyperliquid fundingHistory needs more than ${HL_FUNDING_HISTORY_MAX_PAGES} pages — the result would be incomplete`, { exchange: "hyperliquid" });
2272
+ }
2273
+ const rows = await this._infoPost({ type: "fundingHistory", coin, startTime: from });
2274
+ if (!Array.isArray(rows)) {
2275
+ throw new PerpError("EXCHANGE_ERROR", `Hyperliquid fundingHistory returned no list (${JSON.stringify(rows).slice(0, 120)})`, { exchange: "hyperliquid" });
2276
+ }
2277
+ let last = -Infinity;
2278
+ let fresh = 0;
2279
+ for (const h of rows) {
2280
+ const t = Number(h.time);
2281
+ if (!Number.isFinite(t))
2282
+ throw new PerpError("EXCHANGE_ERROR", `Hyperliquid fundingHistory row without a numeric time: ${JSON.stringify(h).slice(0, 160)}`, { exchange: "hyperliquid" });
2283
+ last = Math.max(last, t);
2284
+ if (!byTime.has(t)) {
2285
+ byTime.set(t, h);
2286
+ fresh++;
2287
+ }
2288
+ }
2289
+ if (rows.length < HL_INFO_PAGE || fresh === 0)
2290
+ break;
2291
+ from = last;
2292
+ }
2293
+ return [...byTime.entries()].sort((a, b) => a[0] - b[0]).map(([, h]) => h);
2294
+ }
1954
2295
  // ────────────────────────────────────────────────────────────
1955
2296
  // Extended methods (from Python SDK / nktkas TS SDK analysis)
1956
2297
  // ────────────────────────────────────────────────────────────
@@ -1958,7 +2299,7 @@ export class HyperliquidAdapter {
1958
2299
  * Place a trigger order (stop loss / take profit).
1959
2300
  * Python SDK: order() with trigger type {"trigger": {triggerPx, isMarket, tpsl: "tp"|"sl"}}
1960
2301
  *
1961
- * Pricing (design §13.10 #3):
2302
+ * Pricing:
1962
2303
  * - isMarket (default): HL native market trigger; `p` = trigger ± 10 % (the venue's documented
1963
2304
  * market TP/SL slippage tolerance, HL_MARKET_TPSL_SLIPPAGE_BPS), aligned toward crossing.
1964
2305
  * - limit trigger with an explicit `limitPrice`: that price, rounded to HL's grid.
@@ -2479,31 +2820,33 @@ export class HyperliquidAdapter {
2479
2820
  return this._sendExchangeAction({ type: "claimRewards" });
2480
2821
  }
2481
2822
  /**
2482
- * List all available HIP-3 deployed perp dexes.
2823
+ * List the HIP-3 deployed perp dexes: allPerpMetas (universe per dex; index 0 = native perps)
2824
+ * joined by index with perpDexs (name, deployer — live 2026-10-19 both list the dexes in the
2825
+ * same order). `assets` holds the LIVE (non-delisted) assets only; `delistedAssets` the rest and
2826
+ * `delisted` is true when every asset of the dex is delisted (km, flx, hyna, cash, vntl, abcd on
2827
+ * 2026-10-19). Delisted dexes stay listed: accounts may still hold positions there.
2483
2828
  */
2484
2829
  async listDeployedDexes() {
2485
- const allMetas = await this._infoPost({ type: "allPerpMetas" });
2830
+ const [allMetas, dexList] = await Promise.all([
2831
+ this._infoPost({ type: "allPerpMetas" }),
2832
+ withCache(`mkt:hl:${this._network}:perpDexs`, HL_MARKET_META_TTL_MS, () => this._infoPost({ type: "perpDexs" })),
2833
+ ]);
2486
2834
  if (!Array.isArray(allMetas))
2487
2835
  return [];
2488
2836
  const dexes = [];
2489
- // allPerpMetas returns an array: [nativePerps, dex1, dex2, ...]
2490
- // Each entry has { universe, marginTables, collateralToken }
2491
- // Dex name is derived from the asset prefix (e.g., "xyz:TSLA" → "xyz")
2492
- // Skip index 0 (native/validator perps — no prefix)
2493
2837
  for (let i = 1; i < allMetas.length; i++) {
2494
- const meta = allMetas[i];
2495
- const universe = (meta.universe ?? []);
2838
+ const universe = (allMetas[i]?.universe ?? []);
2496
2839
  if (universe.length === 0)
2497
2840
  continue;
2498
- // Extract dex name from the first asset's prefix
2499
2841
  const firstAsset = universe[0].name;
2500
2842
  const colonIdx = firstAsset.indexOf(":");
2501
- const dexName = colonIdx > 0 ? firstAsset.slice(0, colonIdx) : `dex-${i}`;
2502
- dexes.push({
2503
- name: dexName,
2504
- deployer: "", // not exposed by API
2505
- assets: universe.map(a => a.name),
2506
- });
2843
+ const prefix = colonIdx > 0 ? firstAsset.slice(0, colonIdx) : `dex-${i}`;
2844
+ const entry = Array.isArray(dexList) ? dexList[i] : null;
2845
+ // The deployer is read from the perpDexs entry of the same dex (by index, checked by name).
2846
+ const deployer = entry?.name === prefix && typeof entry.deployer === "string" ? entry.deployer : "";
2847
+ const live = universe.filter(a => a.isDelisted !== true).map(a => a.name);
2848
+ const gone = universe.filter(a => a.isDelisted === true).map(a => a.name);
2849
+ dexes.push({ name: prefix, deployer, assets: live, delistedAssets: gone, delisted: live.length === 0 });
2507
2850
  }
2508
2851
  return dexes;
2509
2852
  }
@@ -2574,6 +2917,11 @@ export class HyperliquidAdapter {
2574
2917
  // share an aggregated weight limit of 1200 per minute") → typed RATE_LIMITED.
2575
2918
  if (res.status === 429)
2576
2919
  throw rateLimitedError("hyperliquid", res.headers.get("retry-after"), `info ${String(body.type)}: ${text}`);
2920
+ // HTTP 422 "Failed to deserialize the JSON body into the target type" = the request is
2921
+ // malformed (bad interval / field type, live 2026-10-19): a caller error, not retryable.
2922
+ if (res.status === 422) {
2923
+ throw new PerpError("INVALID_PARAMS", `Hyperliquid info ${String(body.type)} rejected the request (422): ${text.slice(0, 200)}`, { exchange: "hyperliquid" });
2924
+ }
2577
2925
  throw new PerpError("EXCHANGE_ERROR", `Hyperliquid info ${String(body.type)} failed (${res.status}): ${text.slice(0, 200)}`, { exchange: "hyperliquid" });
2578
2926
  }
2579
2927
  return res.json();