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
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Derive (derive.xyz, formerly Lyra) v3 public JSON-RPC over HTTP — official API, no auth.
3
+ *
4
+ * - `POST <base>/public/<method>` with the params object as the body → `{id, result}` or
5
+ * `{id, error: {code, message, data}}` (docs.derive.xyz/error-codes.md; live 2026-10-18).
6
+ * - Errors are all PerpErrors, each with a remediation: transport failure → EXCHANGE_UNREACHABLE;
7
+ * HTTP 429 or RPC -32000..-32099 → RATE_LIMITED (retry hint from the RPC `data` "Retry after {ms} ms",
8
+ * else the `retry-after` header); any other RPC error → EXCHANGE_ERROR. A JSON-RPC -32602 is a
9
+ * Derive-side rejection too: user input is validated before any request, so it is never reported as
10
+ * the user's INVALID_PARAMS. Every RPC error carries `details.rpcCode` / `rpcData` so callers can
11
+ * classify the venue code (12001 instrument_not_found → SYMBOL_NOT_FOUND is done by the caller,
12
+ * which knows what was asked).
13
+ * - Public budget (docs rate-limits.md, mainnet reference): 5 TPS per IP, FIXED 5 s window of 25
14
+ * points, 1 point per request. The local bucket (burst 5, 4/s) allows at most 5 + 4·5 = 25 in any 5 s.
15
+ */
16
+ import { PerpError } from "../../errors.js";
17
+ import type { RateLimitPolicy } from "../venue.js";
18
+ import type { Network } from "../venue-types.js";
19
+ export declare const DERIVE_REST_URLS: Readonly<Record<Network, string>>;
20
+ export declare const DERIVE_HTTP_TIMEOUT_MS = 15000;
21
+ export declare const DERIVE_RATE_LIMIT: RateLimitPolicy;
22
+ /** Derive application code for an unknown instrument / currency (`instrument_not_found`). */
23
+ export declare const DERIVE_INSTRUMENT_NOT_FOUND = 12001;
24
+ /** JSON-RPC invalid params — Derive also uses it for "Ticker not found for instrument" (live 2026-10-18). */
25
+ export declare const DERIVE_INVALID_PARAMS = -32602;
26
+ /** Shape drift: an explicit EXCHANGE_ERROR, never a guessed value. */
27
+ export declare function deriveShapeError(what: string): PerpError;
28
+ /** The RPC error code a Derive call failed with, or null (not an RPC error). */
29
+ export declare function deriveRpcCode(e: unknown): number | null;
30
+ export declare class DeriveClient {
31
+ readonly network: Network;
32
+ private readonly baseUrl;
33
+ private readonly rateLimit;
34
+ constructor(network: Network, baseUrl?: string, rateLimit?: RateLimitPolicy);
35
+ /** `public/<method>`; resolves with `result`, every failure is a PerpError. */
36
+ call<T = unknown>(method: string, params: Record<string, unknown>): Promise<T>;
37
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Derive (derive.xyz, formerly Lyra) v3 public JSON-RPC over HTTP — official API, no auth.
3
+ *
4
+ * - `POST <base>/public/<method>` with the params object as the body → `{id, result}` or
5
+ * `{id, error: {code, message, data}}` (docs.derive.xyz/error-codes.md; live 2026-10-18).
6
+ * - Errors are all PerpErrors, each with a remediation: transport failure → EXCHANGE_UNREACHABLE;
7
+ * HTTP 429 or RPC -32000..-32099 → RATE_LIMITED (retry hint from the RPC `data` "Retry after {ms} ms",
8
+ * else the `retry-after` header); any other RPC error → EXCHANGE_ERROR. A JSON-RPC -32602 is a
9
+ * Derive-side rejection too: user input is validated before any request, so it is never reported as
10
+ * the user's INVALID_PARAMS. Every RPC error carries `details.rpcCode` / `rpcData` so callers can
11
+ * classify the venue code (12001 instrument_not_found → SYMBOL_NOT_FOUND is done by the caller,
12
+ * which knows what was asked).
13
+ * - Public budget (docs rate-limits.md, mainnet reference): 5 TPS per IP, FIXED 5 s window of 25
14
+ * points, 1 point per request. The local bucket (burst 5, 4/s) allows at most 5 + 4·5 = 25 in any 5 s.
15
+ */
16
+ import { PerpError, extractErrorMessage } from "../../errors.js";
17
+ import { acquireRate, rateLimitedError } from "../rate-limiter.js";
18
+ export const DERIVE_REST_URLS = {
19
+ mainnet: "https://api.derive.xyz/v3",
20
+ testnet: "https://testnet.api.derive.xyz/v3",
21
+ };
22
+ export const DERIVE_HTTP_TIMEOUT_MS = 15_000;
23
+ export const DERIVE_RATE_LIMIT = { capacity: 5, refillPerSec: 4, weights: null };
24
+ /** Derive application code for an unknown instrument / currency (`instrument_not_found`). */
25
+ export const DERIVE_INSTRUMENT_NOT_FOUND = 12001;
26
+ /** JSON-RPC invalid params — Derive also uses it for "Ticker not found for instrument" (live 2026-10-18). */
27
+ export const DERIVE_INVALID_PARAMS = -32602;
28
+ const UNREACHABLE_REMEDIATION = "Check the network / api.derive.xyz status and retry";
29
+ const RATE_LIMIT_REMEDIATION = "Derive's public budget is 5 requests/s per IP (25 per 5 s window); wait and retry";
30
+ /** Shape drift: an explicit EXCHANGE_ERROR, never a guessed value. */
31
+ export function deriveShapeError(what) {
32
+ return new PerpError("EXCHANGE_ERROR", `Derive API returned an unexpected shape: ${what}`, {
33
+ exchange: "derive",
34
+ remediation: "Derive's v3 API response changed; perp-cli needs an update to read it",
35
+ });
36
+ }
37
+ /** The RPC error code a Derive call failed with, or null (not an RPC error). */
38
+ export function deriveRpcCode(e) {
39
+ if (!(e instanceof PerpError))
40
+ return null;
41
+ const code = e.structured.details?.rpcCode;
42
+ return typeof code === "number" ? code : null;
43
+ }
44
+ export class DeriveClient {
45
+ network;
46
+ baseUrl;
47
+ rateLimit;
48
+ constructor(network, baseUrl = DERIVE_REST_URLS[network], rateLimit = DERIVE_RATE_LIMIT) {
49
+ this.network = network;
50
+ this.baseUrl = baseUrl;
51
+ this.rateLimit = rateLimit;
52
+ }
53
+ /** `public/<method>`; resolves with `result`, every failure is a PerpError. */
54
+ async call(method, params) {
55
+ await acquireRate(`derive:${this.baseUrl}`, this.rateLimit, method);
56
+ const what = `Derive public/${method}`;
57
+ let res;
58
+ try {
59
+ res = await fetch(`${this.baseUrl}/public/${method}`, {
60
+ method: "POST",
61
+ headers: { "content-type": "application/json" },
62
+ body: JSON.stringify(params),
63
+ signal: AbortSignal.timeout(DERIVE_HTTP_TIMEOUT_MS),
64
+ });
65
+ }
66
+ catch (e) {
67
+ throw new PerpError("EXCHANGE_UNREACHABLE", `${what}: ${extractErrorMessage(e)}`, { exchange: "derive", remediation: UNREACHABLE_REMEDIATION });
68
+ }
69
+ const text = await res.text().catch((e) => {
70
+ throw new PerpError("EXCHANGE_UNREACHABLE", `${what}: response body unreadable (${extractErrorMessage(e)})`, {
71
+ exchange: "derive", remediation: UNREACHABLE_REMEDIATION,
72
+ });
73
+ });
74
+ let body;
75
+ try {
76
+ body = JSON.parse(text);
77
+ }
78
+ catch {
79
+ body = undefined;
80
+ }
81
+ const rpcError = body !== null && typeof body === "object" ? body.error : undefined;
82
+ const hasRpcError = rpcError !== undefined && rpcError !== null;
83
+ if (res.status === 429) {
84
+ // The RPC body's "Retry after N ms" is preferred; the retry-after header only when the body has none.
85
+ const fromRpc = hasRpcError ? retryAfterMsOf(rpcError) : null;
86
+ if (fromRpc !== null)
87
+ throw rateLimited(`${what}: HTTP 429`, fromRpc, rpcCodeOf(rpcError));
88
+ throw rateLimitedError("derive", res.headers.get("retry-after"), text);
89
+ }
90
+ if (hasRpcError)
91
+ throw rpcErrorOf(what, rpcError);
92
+ if (!res.ok) {
93
+ throw new PerpError("EXCHANGE_ERROR", `${what}: HTTP ${res.status} ${text.slice(0, 200)}`, {
94
+ exchange: "derive", status: res.status, remediation: "Derive answered with an HTTP error; check api.derive.xyz status and retry",
95
+ });
96
+ }
97
+ if (body === undefined)
98
+ throw deriveShapeError(`${method} returned a non-JSON body`);
99
+ if (body === null || typeof body !== "object" || !("result" in body))
100
+ throw deriveShapeError(`${method} returned no result`);
101
+ return body.result;
102
+ }
103
+ }
104
+ function rpcErrorOf(what, err) {
105
+ if (err === null || typeof err !== "object")
106
+ return deriveShapeError(`${what}: error is not an object`);
107
+ const { code, message, data } = err;
108
+ if (typeof code !== "number" || typeof message !== "string")
109
+ return deriveShapeError(`${what}: error without code / message`);
110
+ const detail = typeof data === "string" && data !== "" && data !== message ? `${message} (${data})` : message;
111
+ if (code <= -32000 && code >= -32099)
112
+ return rateLimited(`${what}: ${detail}`, retryAfterMsOf(err), code);
113
+ return new PerpError("EXCHANGE_ERROR", `${what}: request rejected by Derive: ${detail} [code ${code}]`, {
114
+ exchange: "derive", rpcCode: code, rpcData: typeof data === "string" ? data : null,
115
+ remediation: "Retry later; if it persists, Derive's API changed and perp-cli needs an update",
116
+ });
117
+ }
118
+ function rateLimited(message, retryAfterMs, rpcCode) {
119
+ return new PerpError("RATE_LIMITED", message, {
120
+ exchange: "derive",
121
+ ...(rpcCode !== null ? { rpcCode } : {}),
122
+ ...(retryAfterMs !== null ? { retryAfterMs } : {}),
123
+ remediation: RATE_LIMIT_REMEDIATION,
124
+ });
125
+ }
126
+ function rpcCodeOf(err) {
127
+ const code = err !== null && typeof err === "object" ? err.code : undefined;
128
+ return typeof code === "number" ? code : null;
129
+ }
130
+ /** "Retry after {ms} ms" from an RPC error's `data`, or null. */
131
+ function retryAfterMsOf(err) {
132
+ const data = err !== null && typeof err === "object" ? err.data : undefined;
133
+ const m = typeof data === "string" ? /Retry after (\d+) ?ms/i.exec(data) : null;
134
+ return m !== null ? Number(m[1]) : null;
135
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Derive options — read-only `OptionsVenue` over the v3 public API (official, no auth).
3
+ *
4
+ * Reads (docs.derive.xyz/openapi.json, shapes checked live 2026-10-18):
5
+ * - `get_all_currencies` → currencies with an `option` asset (underlyings).
6
+ * - `get_all_instruments {instrument_type:"option", currency, expired:false, page_size:1000, page}`
7
+ * → instruments; `option_details {expiry (unix s), strike, option_type "C"|"P"}`. Paginated
8
+ * (`pagination.num_pages`, page_size ≤ 1000). `is_active:false` = listed for a future activation
9
+ * (`scheduled_activation`), not tradable and with no ticker — left out of expiries / chains.
10
+ * An unknown currency is RPC 12001.
11
+ * - `get_tickers {instrument_type:"option", currency, expiry_date:"YYYYMMDD"}` → `{tickers: {name: TickerSlim}}`;
12
+ * expiry_date is required for options. ONE call per chain (never one per instrument).
13
+ * - `get_instrument` + `get_ticker` for a single quote (unknown instrument = RPC 12001).
14
+ *
15
+ * Ticker encoding (live): an empty book side is price "0" / size "0" → `null` side; an IV the
16
+ * pricer could not compute (no quote, or a quote below intrinsic) is "0" → `null`; 24 h high / low
17
+ * are "0" when there were no trades → `null`. Greeks are passed through verbatim (a delta of 0 is real).
18
+ */
19
+ import type { Network } from "../venue-types.js";
20
+ import { type OptionChain, type OptionChainQuery, type OptionExpiries, type OptionExpiry, type OptionKind, type OptionQuote, type OptionsVenue, type OptionUnderlyings } from "../options-interface.js";
21
+ import { DeriveClient } from "./client.js";
22
+ /** One option instrument, as perp-cli uses it. */
23
+ export interface DeriveOptionInstrument {
24
+ name: string;
25
+ underlying: string;
26
+ quoteCurrency: string;
27
+ kind: OptionKind;
28
+ strike: string;
29
+ expiryMs: number;
30
+ /** UTC YYYY-MM-DD of the expiry. */
31
+ expiry: string;
32
+ isActive: boolean;
33
+ /** ms epoch of `scheduled_activation`. */
34
+ activationMs: number;
35
+ }
36
+ /** ms epoch → UTC YYYY-MM-DD. */
37
+ export declare function utcDate(ms: number): string;
38
+ /** User expiry, exactly "2026-12-25" or "20261225" (not mixed) → YYYY-MM-DD; anything else INVALID_PARAMS. */
39
+ export declare function normalizeExpiry(input: string): string;
40
+ /** User underlying → venue currency (upper case); INVALID_PARAMS for anything that is not a ticker. */
41
+ export declare function normalizeUnderlying(input: string): string;
42
+ /**
43
+ * CLI option name → Derive wire name: upper-cased, and a decimal point in the strike written as
44
+ * the venue's underscore (`PUMP-20261023-0.0045-C` → `PUMP-20261023-0_0045-C`).
45
+ */
46
+ export declare function deriveInstrumentName(input: string): string;
47
+ export declare function parseDeriveInstrument(raw: unknown): DeriveOptionInstrument;
48
+ /** A ticker_slim snapshot (get_ticker / get_tickers value) of `inst` → OptionQuote. */
49
+ export declare function parseDeriveOptionTicker(raw: unknown, inst: DeriveOptionInstrument): OptionQuote;
50
+ /** Distinct active expiries of `instruments`, soonest first. */
51
+ export declare function expiriesOf(instruments: readonly DeriveOptionInstrument[]): OptionExpiry[];
52
+ /** The `n` strikes closest to `price` (ties → lower strike), returned ascending. */
53
+ export declare function strikesAround(strikes: readonly string[], price: string, n: number): string[];
54
+ /**
55
+ * Chain for one expiry from its instruments and the `get_tickers` map. Rows come from the listed
56
+ * instruments; a listed instrument without a ticker leaves its side null and is named in
57
+ * `missingTickers`. Index / forward are those of the newest ticker (largest `t`) — one ticker's values,
58
+ * not an average over the expiry.
59
+ */
60
+ export declare function buildChain(underlying: string, expiry: OptionExpiry, selection: OptionChain["expirySelection"], instruments: readonly DeriveOptionInstrument[], tickersRaw: unknown, strikesAroundAtm: number | null): OptionChain;
61
+ export declare class DeriveOptionsVenue implements OptionsVenue {
62
+ private readonly client;
63
+ readonly id = "derive";
64
+ readonly displayName = "Derive";
65
+ constructor(client: DeriveClient);
66
+ static forNetwork(network: Network): DeriveOptionsVenue;
67
+ listUnderlyings(): Promise<OptionUnderlyings>;
68
+ /** Every non-expired option instrument of `underlying` (all pages). 12001 → SYMBOL_NOT_FOUND. */
69
+ instruments(underlying: string): Promise<DeriveOptionInstrument[]>;
70
+ listExpiries(underlyingInput: string): Promise<OptionExpiries>;
71
+ getChain(q: OptionChainQuery): Promise<OptionChain>;
72
+ getQuote(instrumentInput: string): Promise<OptionQuote>;
73
+ }
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Derive options — read-only `OptionsVenue` over the v3 public API (official, no auth).
3
+ *
4
+ * Reads (docs.derive.xyz/openapi.json, shapes checked live 2026-10-18):
5
+ * - `get_all_currencies` → currencies with an `option` asset (underlyings).
6
+ * - `get_all_instruments {instrument_type:"option", currency, expired:false, page_size:1000, page}`
7
+ * → instruments; `option_details {expiry (unix s), strike, option_type "C"|"P"}`. Paginated
8
+ * (`pagination.num_pages`, page_size ≤ 1000). `is_active:false` = listed for a future activation
9
+ * (`scheduled_activation`), not tradable and with no ticker — left out of expiries / chains.
10
+ * An unknown currency is RPC 12001.
11
+ * - `get_tickers {instrument_type:"option", currency, expiry_date:"YYYYMMDD"}` → `{tickers: {name: TickerSlim}}`;
12
+ * expiry_date is required for options. ONE call per chain (never one per instrument).
13
+ * - `get_instrument` + `get_ticker` for a single quote (unknown instrument = RPC 12001).
14
+ *
15
+ * Ticker encoding (live): an empty book side is price "0" / size "0" → `null` side; an IV the
16
+ * pricer could not compute (no quote, or a quote below intrinsic) is "0" → `null`; 24 h high / low
17
+ * are "0" when there were no trades → `null`. Greeks are passed through verbatim (a delta of 0 is real).
18
+ */
19
+ import { PerpError } from "../../errors.js";
20
+ import { OPTIONS_SCHEMA, } from "../options-interface.js";
21
+ import { DERIVE_INSTRUMENT_NOT_FOUND, DERIVE_INVALID_PARAMS, DeriveClient, deriveRpcCode, deriveShapeError } from "./client.js";
22
+ const VENUE = "derive";
23
+ const PAGE_SIZE = 1000;
24
+ function obj(v, what) {
25
+ if (v === null || typeof v !== "object" || Array.isArray(v))
26
+ throw deriveShapeError(`${what} is not an object`);
27
+ return v;
28
+ }
29
+ function str(o, key, what) {
30
+ const v = o[key];
31
+ if (typeof v !== "string")
32
+ throw deriveShapeError(`${what}.${key} is not a string`);
33
+ return v;
34
+ }
35
+ /** A decimal string (checked to parse as a finite number; returned verbatim). */
36
+ function dec(o, key, what) {
37
+ const v = str(o, key, what);
38
+ if (v.trim() === "" || !Number.isFinite(Number(v)))
39
+ throw deriveShapeError(`${what}.${key} is not a decimal ("${v}")`);
40
+ return v;
41
+ }
42
+ function int(o, key, what) {
43
+ const v = o[key];
44
+ if (typeof v !== "number" || !Number.isInteger(v))
45
+ throw deriveShapeError(`${what}.${key} is not an integer`);
46
+ return v;
47
+ }
48
+ /** Derive's "0" placeholder → null; anything else verbatim. */
49
+ function nonZero(v) {
50
+ return Number(v) === 0 ? null : v;
51
+ }
52
+ // ── dates ──
53
+ /** ms epoch → UTC YYYY-MM-DD. */
54
+ export function utcDate(ms) {
55
+ return new Date(ms).toISOString().slice(0, 10);
56
+ }
57
+ /** User expiry, exactly "2026-12-25" or "20261225" (not mixed) → YYYY-MM-DD; anything else INVALID_PARAMS. */
58
+ export function normalizeExpiry(input) {
59
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(input.trim()) ?? /^(\d{4})(\d{2})(\d{2})$/.exec(input.trim());
60
+ const iso = m !== null ? `${m[1]}-${m[2]}-${m[3]}` : null;
61
+ if (iso === null || Number.isNaN(Date.parse(`${iso}T00:00:00Z`)) || utcDate(Date.parse(`${iso}T00:00:00Z`)) !== iso) {
62
+ throw new PerpError("INVALID_PARAMS", `Invalid expiry "${input}" — expected YYYY-MM-DD (UTC)`, {
63
+ remediation: `List expiries: perp -e ${VENUE} options expiries <UNDERLYING>`,
64
+ });
65
+ }
66
+ return iso;
67
+ }
68
+ /** User underlying → venue currency (upper case); INVALID_PARAMS for anything that is not a ticker. */
69
+ export function normalizeUnderlying(input) {
70
+ const u = input.trim().toUpperCase();
71
+ if (!/^[A-Z0-9_]{1,20}$/.test(u)) {
72
+ throw new PerpError("INVALID_PARAMS", `Invalid underlying "${input}"`, {
73
+ remediation: `List underlyings: perp -e ${VENUE} options underlyings`,
74
+ });
75
+ }
76
+ return u;
77
+ }
78
+ /**
79
+ * CLI option name → Derive wire name: upper-cased, and a decimal point in the strike written as
80
+ * the venue's underscore (`PUMP-20261023-0.0045-C` → `PUMP-20261023-0_0045-C`).
81
+ */
82
+ export function deriveInstrumentName(input) {
83
+ const parts = input.trim().toUpperCase().split("-");
84
+ if (parts.length === 4)
85
+ parts[2] = parts[2].replace(/\./g, "_");
86
+ return parts.join("-");
87
+ }
88
+ // ── parsing ──
89
+ export function parseDeriveInstrument(raw) {
90
+ const o = obj(raw, "instrument");
91
+ const name = str(o, "instrument_name", "instrument");
92
+ const what = `instrument ${name}`;
93
+ if (str(o, "instrument_type", what) !== "option")
94
+ throw deriveShapeError(`${what} is not an option`);
95
+ const d = obj(o.option_details, `${what}.option_details`);
96
+ const t = str(d, "option_type", `${what}.option_details`);
97
+ if (t !== "C" && t !== "P")
98
+ throw deriveShapeError(`${what}.option_details.option_type "${t}" is not C / P`);
99
+ const expiryMs = int(d, "expiry", `${what}.option_details`) * 1000;
100
+ const isActive = o.is_active;
101
+ if (typeof isActive !== "boolean")
102
+ throw deriveShapeError(`${what}.is_active is not a boolean`);
103
+ return {
104
+ name,
105
+ underlying: str(o, "base_currency", what),
106
+ quoteCurrency: str(o, "quote_currency", what),
107
+ kind: t === "C" ? "call" : "put",
108
+ strike: dec(d, "strike", `${what}.option_details`),
109
+ expiryMs,
110
+ expiry: utcDate(expiryMs),
111
+ isActive,
112
+ activationMs: int(o, "scheduled_activation", what) * 1000,
113
+ };
114
+ }
115
+ /** A ticker_slim snapshot (get_ticker / get_tickers value) of `inst` → OptionQuote. */
116
+ export function parseDeriveOptionTicker(raw, inst) {
117
+ const what = `ticker ${inst.name}`;
118
+ const t = obj(raw, what);
119
+ const bidPx = dec(t, "b", what), bidSz = dec(t, "B", what);
120
+ const askPx = dec(t, "a", what), askSz = dec(t, "A", what);
121
+ const bid = Number(bidPx) === 0 ? null : { price: bidPx, size: bidSz };
122
+ const ask = Number(askPx) === 0 ? null : { price: askPx, size: askSz };
123
+ const s = obj(t.stats, `${what}.stats`);
124
+ const trades = int(s, "n", `${what}.stats`);
125
+ const pr = t.option_pricing;
126
+ let p = null;
127
+ if (pr !== null && pr !== undefined)
128
+ p = obj(pr, `${what}.option_pricing`);
129
+ const pw = `${what}.option_pricing`;
130
+ return {
131
+ schema: OPTIONS_SCHEMA,
132
+ venue: VENUE,
133
+ instrument: inst.name,
134
+ underlying: inst.underlying,
135
+ quoteCurrency: inst.quoteCurrency,
136
+ kind: inst.kind,
137
+ strike: inst.strike,
138
+ expiry: inst.expiry,
139
+ expiryMs: inst.expiryMs,
140
+ bid,
141
+ ask,
142
+ mark: dec(t, "M", what),
143
+ index: dec(t, "I", what),
144
+ forward: p === null ? null : dec(p, "f", pw),
145
+ discountFactor: p === null ? null : dec(p, "df", pw),
146
+ iv: {
147
+ mark: p === null ? null : nonZero(dec(p, "i", pw)),
148
+ bid: p === null || bid === null ? null : nonZero(dec(p, "bi", pw)),
149
+ ask: p === null || ask === null ? null : nonZero(dec(p, "ai", pw)),
150
+ },
151
+ greeks: p === null ? null : {
152
+ delta: dec(p, "d", pw), gamma: dec(p, "g", pw), theta: dec(p, "t", pw), vega: dec(p, "v", pw), rho: dec(p, "r", pw),
153
+ },
154
+ openInterest: dec(s, "oi", `${what}.stats`),
155
+ stats24h: {
156
+ contracts: dec(s, "c", `${what}.stats`),
157
+ notional: dec(s, "v", `${what}.stats`),
158
+ premium: dec(s, "pr", `${what}.stats`),
159
+ trades,
160
+ high: trades === 0 ? null : dec(s, "h", `${what}.stats`),
161
+ low: trades === 0 ? null : dec(s, "l", `${what}.stats`),
162
+ },
163
+ timestampMs: int(t, "t", what),
164
+ };
165
+ }
166
+ /** Distinct active expiries of `instruments`, soonest first. */
167
+ export function expiriesOf(instruments) {
168
+ const by = new Map();
169
+ for (const i of instruments) {
170
+ if (!i.isActive)
171
+ continue;
172
+ const e = by.get(i.expiryMs) ?? { strikes: new Set(), n: 0 };
173
+ e.strikes.add(i.strike);
174
+ e.n++;
175
+ by.set(i.expiryMs, e);
176
+ }
177
+ return [...by.entries()]
178
+ .sort((a, b) => a[0] - b[0])
179
+ .map(([ms, e]) => ({ expiry: utcDate(ms), expiryMs: ms, strikes: e.strikes.size, instruments: e.n }));
180
+ }
181
+ /** The `n` strikes closest to `price` (ties → lower strike), returned ascending. */
182
+ export function strikesAround(strikes, price, n) {
183
+ const p = Number(price);
184
+ const nearest = [...strikes].sort((a, b) => (Math.abs(Number(a) - p) - Math.abs(Number(b) - p)) || (Number(a) - Number(b)));
185
+ return nearest.slice(0, n).sort((a, b) => Number(a) - Number(b));
186
+ }
187
+ /**
188
+ * Chain for one expiry from its instruments and the `get_tickers` map. Rows come from the listed
189
+ * instruments; a listed instrument without a ticker leaves its side null and is named in
190
+ * `missingTickers`. Index / forward are those of the newest ticker (largest `t`) — one ticker's values,
191
+ * not an average over the expiry.
192
+ */
193
+ export function buildChain(underlying, expiry, selection, instruments, tickersRaw, strikesAroundAtm) {
194
+ const tickers = obj(tickersRaw, "get_tickers result.tickers");
195
+ const rowsByStrike = new Map();
196
+ const missingTickers = [];
197
+ let latest = null;
198
+ for (const inst of instruments) {
199
+ const row = rowsByStrike.get(inst.strike) ?? { strike: inst.strike, call: null, put: null };
200
+ rowsByStrike.set(inst.strike, row);
201
+ if (!(inst.name in tickers)) {
202
+ missingTickers.push(inst.name);
203
+ continue;
204
+ }
205
+ const q = parseDeriveOptionTicker(tickers[inst.name], inst);
206
+ row[inst.kind] = q;
207
+ if (latest === null || q.timestampMs > latest.timestampMs)
208
+ latest = q;
209
+ }
210
+ const strikes = [...rowsByStrike.keys()];
211
+ let atm = null;
212
+ if (latest !== null) {
213
+ const reference = latest.forward !== null ? "forward" : "index";
214
+ const price = latest.forward !== null ? latest.forward : latest.index;
215
+ atm = { strike: strikesAround(strikes, price, 1)[0], reference, price };
216
+ }
217
+ let keep = strikes;
218
+ if (strikesAroundAtm !== null) {
219
+ if (atm === null) {
220
+ throw new PerpError("EXCHANGE_ERROR", `Derive returned no ticker for ${underlying} ${expiry.expiry}; the ATM strike for --strikes is unknown`, {
221
+ exchange: VENUE, remediation: `Run without --strikes to see the listed strikes`,
222
+ });
223
+ }
224
+ keep = strikesAround(strikes, atm.price, strikesAroundAtm);
225
+ }
226
+ const rows = keep
227
+ .sort((a, b) => Number(a) - Number(b))
228
+ .map(k => rowsByStrike.get(k));
229
+ return {
230
+ schema: OPTIONS_SCHEMA,
231
+ venue: VENUE,
232
+ underlying,
233
+ expiry: expiry.expiry,
234
+ expiryMs: expiry.expiryMs,
235
+ expirySelection: selection,
236
+ index: latest === null ? null : latest.index,
237
+ forward: latest === null ? null : latest.forward,
238
+ atm,
239
+ strikesListed: strikes.length,
240
+ rows,
241
+ missingTickers: missingTickers.sort(),
242
+ };
243
+ }
244
+ // ── venue ──
245
+ function noOptions(underlying) {
246
+ return new PerpError("SYMBOL_NOT_FOUND", `Derive has no ${underlying} options`, {
247
+ exchange: VENUE, remediation: `List underlyings: perp -e ${VENUE} options underlyings`,
248
+ });
249
+ }
250
+ export class DeriveOptionsVenue {
251
+ client;
252
+ id = VENUE;
253
+ displayName = "Derive";
254
+ constructor(client) {
255
+ this.client = client;
256
+ }
257
+ static forNetwork(network) {
258
+ return new DeriveOptionsVenue(new DeriveClient(network));
259
+ }
260
+ async listUnderlyings() {
261
+ const raw = await this.client.call("get_all_currencies", {});
262
+ if (!Array.isArray(raw))
263
+ throw deriveShapeError("get_all_currencies result is not a list");
264
+ const underlyings = raw
265
+ .map((c, i) => obj(c, `currency #${i}`))
266
+ .filter(c => c.option !== null && c.option !== undefined)
267
+ .map(c => {
268
+ const what = `currency ${String(c.currency)}`;
269
+ return { underlying: str(c, "currency", what), spotPrice: c.spot_price === null ? null : dec(c, "spot_price", what) };
270
+ })
271
+ .sort((a, b) => a.underlying.localeCompare(b.underlying));
272
+ return { schema: OPTIONS_SCHEMA, venue: VENUE, underlyings };
273
+ }
274
+ /** Every non-expired option instrument of `underlying` (all pages). 12001 → SYMBOL_NOT_FOUND. */
275
+ async instruments(underlying) {
276
+ const out = [];
277
+ for (let page = 1, pages = 1; page <= pages; page++) {
278
+ let res;
279
+ try {
280
+ res = await this.client.call("get_all_instruments", {
281
+ instrument_type: "option", currency: underlying, expired: false, page_size: PAGE_SIZE, page,
282
+ });
283
+ }
284
+ catch (e) {
285
+ if (deriveRpcCode(e) === DERIVE_INSTRUMENT_NOT_FOUND)
286
+ throw noOptions(underlying);
287
+ throw e;
288
+ }
289
+ const r = obj(res, "get_all_instruments result");
290
+ if (!Array.isArray(r.instruments))
291
+ throw deriveShapeError("get_all_instruments result.instruments is not a list");
292
+ pages = int(obj(r.pagination, "get_all_instruments result.pagination"), "num_pages", "pagination");
293
+ for (const i of r.instruments)
294
+ out.push(parseDeriveInstrument(i));
295
+ }
296
+ return out;
297
+ }
298
+ async listExpiries(underlyingInput) {
299
+ const underlying = normalizeUnderlying(underlyingInput);
300
+ const expiries = expiriesOf(await this.instruments(underlying));
301
+ if (expiries.length === 0)
302
+ throw noOptions(underlying);
303
+ return { schema: OPTIONS_SCHEMA, venue: VENUE, underlying, expiries };
304
+ }
305
+ async getChain(q) {
306
+ const underlying = normalizeUnderlying(q.underlying);
307
+ const wanted = q.expiry === null ? null : normalizeExpiry(q.expiry);
308
+ if (q.strikesAroundAtm !== null && (!Number.isInteger(q.strikesAroundAtm) || q.strikesAroundAtm < 1)) {
309
+ throw new PerpError("INVALID_PARAMS", `--strikes must be a positive integer (got ${q.strikesAroundAtm})`, {
310
+ remediation: "Pass a positive integer, e.g. --strikes 10",
311
+ });
312
+ }
313
+ const all = await this.instruments(underlying);
314
+ const expiries = expiriesOf(all);
315
+ if (expiries.length === 0)
316
+ throw noOptions(underlying);
317
+ const expiry = wanted === null ? expiries[0] : expiries.find(e => e.expiry === wanted);
318
+ if (expiry === undefined) {
319
+ throw new PerpError("SYMBOL_NOT_FOUND", `Derive has no active ${underlying} option expiry ${wanted}`, {
320
+ exchange: VENUE,
321
+ remediation: `Active expiries: ${expiries.map(e => e.expiry).join(", ")} (perp -e ${VENUE} options expiries ${underlying})`,
322
+ });
323
+ }
324
+ const listed = all.filter(i => i.isActive && i.expiryMs === expiry.expiryMs);
325
+ const res = obj(await this.client.call("get_tickers", {
326
+ instrument_type: "option", currency: underlying, expiry_date: expiry.expiry.replace(/-/g, ""),
327
+ }), "get_tickers result");
328
+ return buildChain(underlying, expiry, wanted === null ? "nearest" : "requested", listed, res.tickers, q.strikesAroundAtm);
329
+ }
330
+ async getQuote(instrumentInput) {
331
+ const name = deriveInstrumentName(instrumentInput);
332
+ // Both reads are needed (instrument facts + live ticker); issued together, judged instrument first.
333
+ const [instR, tickR] = await Promise.allSettled([
334
+ this.client.call("get_instrument", { instrument_name: name }),
335
+ this.client.call("get_ticker", { instrument_name: name }),
336
+ ]);
337
+ if (instR.status === "rejected") {
338
+ if (deriveRpcCode(instR.reason) === DERIVE_INSTRUMENT_NOT_FOUND) {
339
+ throw new PerpError("SYMBOL_NOT_FOUND", `Derive has no instrument ${name}`, {
340
+ exchange: VENUE, remediation: `Find option names: perp -e ${VENUE} options chain <UNDERLYING>`,
341
+ });
342
+ }
343
+ throw instR.reason;
344
+ }
345
+ const o = obj(instR.value, "get_instrument result");
346
+ if (o.instrument_type !== "option") {
347
+ throw new PerpError("INVALID_PARAMS", `${name} is a Derive ${String(o.instrument_type)}, not an option`, {
348
+ exchange: VENUE, remediation: `Find option names: perp -e ${VENUE} options chain <UNDERLYING>`,
349
+ });
350
+ }
351
+ const inst = parseDeriveInstrument(o);
352
+ if (!inst.isActive && inst.expiryMs <= Date.now()) {
353
+ // Expired (live: ETH-20261019-2400-C, is_active false after expiry, option_details.settlement_price set).
354
+ const sp = o.option_details?.settlement_price;
355
+ const settled = typeof sp === "string" && sp !== "" ? `, settled at ${sp}` : " (settlement price not published yet)";
356
+ throw new PerpError("SYMBOL_NOT_FOUND", `Derive option ${name} expired at ${new Date(inst.expiryMs).toISOString()}${settled}`, {
357
+ exchange: VENUE, expired: true, settlementPrice: typeof sp === "string" && sp !== "" ? sp : null,
358
+ remediation: `Active expiries: perp -e ${VENUE} options expiries ${inst.underlying}`,
359
+ });
360
+ }
361
+ if (!inst.isActive) {
362
+ throw new PerpError("SYMBOL_NOT_FOUND", `Derive option ${name} is listed but not active (activation ${new Date(inst.activationMs).toISOString()})`, {
363
+ exchange: VENUE, remediation: `Active expiries: perp -e ${VENUE} options expiries ${inst.underlying}`,
364
+ });
365
+ }
366
+ if (tickR.status === "rejected") {
367
+ // An active instrument with no ticker (-32602 "Ticker not found for instrument", live): an
368
+ // activation / expiry race on Derive's side — transient, never the user's INVALID_PARAMS.
369
+ if (deriveRpcCode(tickR.reason) === DERIVE_INVALID_PARAMS) {
370
+ throw new PerpError("EXCHANGE_ERROR", `Derive has no live ticker for active option ${name} (${tickR.reason instanceof Error ? tickR.reason.message : String(tickR.reason)})`, {
371
+ exchange: VENUE, rpcCode: DERIVE_INVALID_PARAMS,
372
+ remediation: `The option may be activating or expiring right now; retry in a moment, or list live instruments: perp -e ${VENUE} options chain ${inst.underlying} --expiry ${inst.expiry}`,
373
+ });
374
+ }
375
+ throw tickR.reason;
376
+ }
377
+ return parseDeriveOptionTicker(tickR.value, inst);
378
+ }
379
+ }