perp-cli 0.14.3 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/README.md +152 -13
  2. package/dist/agent-wallet/store.d.ts +3 -2
  3. package/dist/agent-wallet/store.js +2 -2
  4. package/dist/api/public/hyperliquid.js +4 -0
  5. package/dist/api/public/pacifica.js +83 -13
  6. package/dist/api/public/urls.d.ts +2 -0
  7. package/dist/api/public/urls.js +3 -0
  8. package/dist/arb/utils.d.ts +1 -2
  9. package/dist/arb/utils.js +1 -2
  10. package/dist/bot/apex/pulse.js +3 -0
  11. package/dist/bot/apex/radar.js +2 -1
  12. package/dist/bot/engine.d.ts +10 -1
  13. package/dist/bot/engine.js +14 -6
  14. package/dist/bot/indicators.d.ts +7 -0
  15. package/dist/bot/indicators.js +16 -0
  16. package/dist/bot/perp-order.d.ts +4 -4
  17. package/dist/bot/perp-order.js +5 -5
  18. package/dist/bot/quarantine.d.ts +1 -1
  19. package/dist/bot/quarantine.js +1 -1
  20. package/dist/bot/strategies/apex-strategy.js +3 -2
  21. package/dist/bot/strategies/engine-mm.js +6 -5
  22. package/dist/bot/strategies/funding-arb-strategy.js +2 -2
  23. package/dist/bot/strategies/funding-arb-utils.d.ts +1 -1
  24. package/dist/bot/strategies/funding-arb-utils.js +5 -5
  25. package/dist/bot/strategies/funding-auto.js +2 -2
  26. package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
  27. package/dist/bot/strategies/liquidation-mm.js +8 -2
  28. package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
  29. package/dist/bot/strategies/momentum-breakout.js +11 -3
  30. package/dist/bot/strategies/spot-perp-arb-strategy.js +2 -2
  31. package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
  32. package/dist/bot/strategies/spot-perp-executor.js +1 -1
  33. package/dist/bot/strategy-types.d.ts +2 -1
  34. package/dist/bot/tui/BotDashboard.d.ts +1 -1
  35. package/dist/bot/tui/BotDashboard.js +1 -1
  36. package/dist/cache.js +12 -6
  37. package/dist/cli-program.d.ts +15 -0
  38. package/dist/cli-program.js +125 -5
  39. package/dist/commands/agent-import.d.ts +7 -0
  40. package/dist/commands/agent-import.js +30 -1
  41. package/dist/commands/agent.d.ts +6 -3
  42. package/dist/commands/agent.js +160 -12
  43. package/dist/commands/arb/index.js +4 -4
  44. package/dist/commands/arb-auto.js +21 -17
  45. package/dist/commands/arb.d.ts +12 -0
  46. package/dist/commands/arb.js +61 -143
  47. package/dist/commands/bot.js +1 -1
  48. package/dist/commands/builder.js +1 -1
  49. package/dist/commands/funds.js +113 -3
  50. package/dist/commands/init.d.ts +1 -1
  51. package/dist/commands/init.js +27 -7
  52. package/dist/commands/manage.js +23 -21
  53. package/dist/commands/market.js +167 -35
  54. package/dist/commands/options.d.ts +9 -0
  55. package/dist/commands/options.js +134 -0
  56. package/dist/commands/pacifica-earn.d.ts +5 -0
  57. package/dist/commands/pacifica-earn.js +468 -0
  58. package/dist/commands/pacifica-orders.d.ts +14 -0
  59. package/dist/commands/pacifica-orders.js +142 -0
  60. package/dist/commands/portfolio.js +32 -47
  61. package/dist/commands/rebalance.d.ts +1 -0
  62. package/dist/commands/rebalance.js +15 -0
  63. package/dist/commands/risk.d.ts +11 -2
  64. package/dist/commands/risk.js +39 -44
  65. package/dist/commands/trade.d.ts +1 -0
  66. package/dist/commands/trade.js +63 -58
  67. package/dist/commands/wallet.js +1 -1
  68. package/dist/config.d.ts +2 -2
  69. package/dist/config.js +15 -4
  70. package/dist/constants.js +6 -0
  71. package/dist/dashboard/server.js +1 -1
  72. package/dist/errors.d.ts +24 -1
  73. package/dist/errors.js +25 -3
  74. package/dist/exchanges/arcus/adapter.d.ts +7 -7
  75. package/dist/exchanges/arcus/adapter.js +12 -12
  76. package/dist/exchanges/arcus/agent.d.ts +1 -1
  77. package/dist/exchanges/arcus/agent.js +1 -1
  78. package/dist/exchanges/arcus/client.js +1 -1
  79. package/dist/exchanges/arcus/signing.d.ts +2 -3
  80. package/dist/exchanges/arcus/signing.js +1 -1
  81. package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
  82. package/dist/exchanges/arcus/spot-chain.js +1 -1
  83. package/dist/exchanges/arcus/spot-router.d.ts +1 -1
  84. package/dist/exchanges/arcus/spot-router.js +2 -2
  85. package/dist/exchanges/arcus/spot.d.ts +2 -3
  86. package/dist/exchanges/arcus/spot.js +2 -3
  87. package/dist/exchanges/aster-spot.d.ts +3 -3
  88. package/dist/exchanges/aster-spot.js +6 -9
  89. package/dist/exchanges/aster-typed-data.d.ts +1 -1
  90. package/dist/exchanges/aster-typed-data.js +1 -1
  91. package/dist/exchanges/aster.d.ts +4 -4
  92. package/dist/exchanges/aster.js +31 -13
  93. package/dist/exchanges/builder-fee.d.ts +1 -1
  94. package/dist/exchanges/builder-fee.js +13 -2
  95. package/dist/exchanges/canonical-symbols.d.ts +1 -2
  96. package/dist/exchanges/canonical-symbols.js +26 -2
  97. package/dist/exchanges/derive/client.d.ts +37 -0
  98. package/dist/exchanges/derive/client.js +135 -0
  99. package/dist/exchanges/derive/options.d.ts +68 -0
  100. package/dist/exchanges/derive/options.js +360 -0
  101. package/dist/exchanges/extended/adapter.d.ts +82 -0
  102. package/dist/exchanges/extended/adapter.js +179 -0
  103. package/dist/exchanges/extended/client.d.ts +38 -0
  104. package/dist/exchanges/extended/client.js +88 -0
  105. package/dist/exchanges/extended/public.d.ts +130 -0
  106. package/dist/exchanges/extended/public.js +429 -0
  107. package/dist/exchanges/grvt/adapter.d.ts +86 -0
  108. package/dist/exchanges/grvt/adapter.js +206 -0
  109. package/dist/exchanges/grvt/client.d.ts +25 -0
  110. package/dist/exchanges/grvt/client.js +86 -0
  111. package/dist/exchanges/grvt/public.d.ts +151 -0
  112. package/dist/exchanges/grvt/public.js +438 -0
  113. package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
  114. package/dist/exchanges/hyperliquid-spot.js +5 -5
  115. package/dist/exchanges/hyperliquid.d.ts +22 -12
  116. package/dist/exchanges/hyperliquid.js +80 -34
  117. package/dist/exchanges/index.d.ts +1 -1
  118. package/dist/exchanges/index.js +1 -1
  119. package/dist/exchanges/interface.d.ts +94 -6
  120. package/dist/exchanges/interface.js +16 -0
  121. package/dist/exchanges/ioc-outcome.d.ts +1 -1
  122. package/dist/exchanges/ioc-outcome.js +1 -1
  123. package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
  124. package/dist/exchanges/legacy-trading-v2.js +7 -2
  125. package/dist/exchanges/lighter-public.d.ts +2 -2
  126. package/dist/exchanges/lighter-public.js +2 -2
  127. package/dist/exchanges/lighter-signer.d.ts +1 -1
  128. package/dist/exchanges/lighter-signer.js +1 -1
  129. package/dist/exchanges/lighter-spot.d.ts +3 -4
  130. package/dist/exchanges/lighter-spot.js +6 -7
  131. package/dist/exchanges/lighter.d.ts +11 -11
  132. package/dist/exchanges/lighter.js +28 -14
  133. package/dist/exchanges/ondo/adapter.d.ts +5 -6
  134. package/dist/exchanges/ondo/adapter.js +16 -14
  135. package/dist/exchanges/ondo/agent.d.ts +1 -1
  136. package/dist/exchanges/ondo/agent.js +1 -1
  137. package/dist/exchanges/ondo/client.d.ts +1 -1
  138. package/dist/exchanges/ondo/client.js +2 -2
  139. package/dist/exchanges/ondo/public.d.ts +8 -1
  140. package/dist/exchanges/ondo/public.js +13 -2
  141. package/dist/exchanges/ondo/spot.d.ts +2 -2
  142. package/dist/exchanges/ondo/spot.js +3 -3
  143. package/dist/exchanges/options-interface.d.ts +158 -0
  144. package/dist/exchanges/options-interface.js +17 -0
  145. package/dist/exchanges/options-venues.d.ts +12 -0
  146. package/dist/exchanges/options-venues.js +35 -0
  147. package/dist/exchanges/pacifica-atomic.d.ts +211 -0
  148. package/dist/exchanges/pacifica-atomic.js +440 -0
  149. package/dist/exchanges/pacifica-decimal.d.ts +10 -0
  150. package/dist/exchanges/pacifica-decimal.js +51 -0
  151. package/dist/exchanges/pacifica-presigned.d.ts +53 -0
  152. package/dist/exchanges/pacifica-presigned.js +141 -0
  153. package/dist/exchanges/pacifica-print.d.ts +200 -0
  154. package/dist/exchanges/pacifica-print.js +417 -0
  155. package/dist/exchanges/pacifica-spot.d.ts +2 -2
  156. package/dist/exchanges/pacifica-spot.js +3 -3
  157. package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
  158. package/dist/exchanges/pacifica-typed-data.js +1 -1
  159. package/dist/exchanges/pacifica-vaults.d.ts +121 -0
  160. package/dist/exchanges/pacifica-vaults.js +264 -0
  161. package/dist/exchanges/pacifica.d.ts +32 -5
  162. package/dist/exchanges/pacifica.js +114 -48
  163. package/dist/exchanges/precision.d.ts +3 -1
  164. package/dist/exchanges/precision.js +9 -2
  165. package/dist/exchanges/qfex/adapter.d.ts +18 -5
  166. package/dist/exchanges/qfex/adapter.js +32 -12
  167. package/dist/exchanges/qfex/agent.d.ts +1 -1
  168. package/dist/exchanges/qfex/agent.js +1 -1
  169. package/dist/exchanges/qfex/client.d.ts +1 -1
  170. package/dist/exchanges/qfex/client.js +1 -1
  171. package/dist/exchanges/qfex/errors.d.ts +1 -1
  172. package/dist/exchanges/qfex/errors.js +2 -2
  173. package/dist/exchanges/qfex/public.d.ts +3 -3
  174. package/dist/exchanges/qfex/public.js +3 -3
  175. package/dist/exchanges/qfex/signing.d.ts +2 -2
  176. package/dist/exchanges/qfex/signing.js +4 -5
  177. package/dist/exchanges/qfex/ws-transport.d.ts +2 -2
  178. package/dist/exchanges/qfex/ws-transport.js +3 -3
  179. package/dist/exchanges/rate-limiter.d.ts +1 -1
  180. package/dist/exchanges/rate-limiter.js +5 -2
  181. package/dist/exchanges/registry.d.ts +1 -1
  182. package/dist/exchanges/registry.js +1 -1
  183. package/dist/exchanges/risex/adapter.d.ts +148 -0
  184. package/dist/exchanges/risex/adapter.js +796 -0
  185. package/dist/exchanges/risex/agent.d.ts +154 -0
  186. package/dist/exchanges/risex/agent.js +384 -0
  187. package/dist/exchanges/risex/client.d.ts +47 -0
  188. package/dist/exchanges/risex/client.js +174 -0
  189. package/dist/exchanges/risex/nonce.d.ts +43 -0
  190. package/dist/exchanges/risex/nonce.js +212 -0
  191. package/dist/exchanges/risex/public.d.ts +64 -0
  192. package/dist/exchanges/risex/public.js +150 -0
  193. package/dist/exchanges/risex/session.d.ts +71 -0
  194. package/dist/exchanges/risex/session.js +171 -0
  195. package/dist/exchanges/risex/typed-data.d.ts +173 -0
  196. package/dist/exchanges/risex/typed-data.js +240 -0
  197. package/dist/exchanges/risex/units.d.ts +29 -0
  198. package/dist/exchanges/risex/units.js +104 -0
  199. package/dist/exchanges/slippage.d.ts +1 -1
  200. package/dist/exchanges/slippage.js +1 -1
  201. package/dist/exchanges/spot-factory.d.ts +3 -3
  202. package/dist/exchanges/spot-factory.js +7 -7
  203. package/dist/exchanges/spot-interface.d.ts +5 -5
  204. package/dist/exchanges/spot-interface.js +2 -2
  205. package/dist/exchanges/spot-result.d.ts +3 -3
  206. package/dist/exchanges/spot-result.js +3 -3
  207. package/dist/exchanges/trading-v2.d.ts +3 -3
  208. package/dist/exchanges/trading-v2.js +9 -6
  209. package/dist/exchanges/txflow/adapter.d.ts +54 -0
  210. package/dist/exchanges/txflow/adapter.js +118 -0
  211. package/dist/exchanges/txflow/client.d.ts +47 -0
  212. package/dist/exchanges/txflow/client.js +153 -0
  213. package/dist/exchanges/txflow/public.d.ts +76 -0
  214. package/dist/exchanges/txflow/public.js +284 -0
  215. package/dist/exchanges/unknown-symbol.d.ts +29 -0
  216. package/dist/exchanges/unknown-symbol.js +51 -0
  217. package/dist/exchanges/variational/adapter.d.ts +71 -0
  218. package/dist/exchanges/variational/adapter.js +137 -0
  219. package/dist/exchanges/variational/client.d.ts +21 -0
  220. package/dist/exchanges/variational/client.js +66 -0
  221. package/dist/exchanges/variational/public.d.ts +126 -0
  222. package/dist/exchanges/variational/public.js +430 -0
  223. package/dist/exchanges/venue-types.d.ts +6 -7
  224. package/dist/exchanges/venue-types.js +2 -2
  225. package/dist/exchanges/venue.d.ts +51 -8
  226. package/dist/exchanges/venue.js +54 -4
  227. package/dist/exchanges/venues/arcus.d.ts +1 -2
  228. package/dist/exchanges/venues/arcus.js +5 -4
  229. package/dist/exchanges/venues/aster.d.ts +2 -2
  230. package/dist/exchanges/venues/aster.js +6 -4
  231. package/dist/exchanges/venues/create-helpers.d.ts +1 -1
  232. package/dist/exchanges/venues/create-helpers.js +3 -3
  233. package/dist/exchanges/venues/extended.d.ts +21 -0
  234. package/dist/exchanges/venues/extended.js +96 -0
  235. package/dist/exchanges/venues/grvt.d.ts +30 -0
  236. package/dist/exchanges/venues/grvt.js +98 -0
  237. package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
  238. package/dist/exchanges/venues/hyperliquid.js +10 -8
  239. package/dist/exchanges/venues/index.d.ts +7 -2
  240. package/dist/exchanges/venues/index.js +8 -2
  241. package/dist/exchanges/venues/lighter-family.d.ts +7 -9
  242. package/dist/exchanges/venues/lighter-family.js +9 -11
  243. package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
  244. package/dist/exchanges/venues/lighter-rh.js +7 -5
  245. package/dist/exchanges/venues/lighter.d.ts +1 -1
  246. package/dist/exchanges/venues/lighter.js +5 -3
  247. package/dist/exchanges/venues/ondo.d.ts +1 -1
  248. package/dist/exchanges/venues/ondo.js +5 -3
  249. package/dist/exchanges/venues/pacifica.d.ts +2 -2
  250. package/dist/exchanges/venues/pacifica.js +6 -4
  251. package/dist/exchanges/venues/qfex.d.ts +1 -1
  252. package/dist/exchanges/venues/qfex.js +4 -2
  253. package/dist/exchanges/venues/risex-release.d.ts +14 -0
  254. package/dist/exchanges/venues/risex-release.js +14 -0
  255. package/dist/exchanges/venues/risex.d.ts +31 -0
  256. package/dist/exchanges/venues/risex.js +116 -0
  257. package/dist/exchanges/venues/txflow.d.ts +18 -0
  258. package/dist/exchanges/venues/txflow.js +77 -0
  259. package/dist/exchanges/venues/variational.d.ts +21 -0
  260. package/dist/exchanges/venues/variational.js +85 -0
  261. package/dist/execution-log.d.ts +1 -1
  262. package/dist/funding/legacy-public.d.ts +1 -1
  263. package/dist/funding/legacy-public.js +9 -4
  264. package/dist/funding/normalize.d.ts +1 -1
  265. package/dist/funding/normalize.js +1 -1
  266. package/dist/funding/rates.d.ts +44 -4
  267. package/dist/funding/rates.js +56 -12
  268. package/dist/index.js +7 -6
  269. package/dist/jobs.d.ts +1 -0
  270. package/dist/jobs.js +25 -1
  271. package/dist/landing.d.ts +2 -0
  272. package/dist/landing.js +1 -1
  273. package/dist/mcp-pacifica-earn.d.ts +9 -0
  274. package/dist/mcp-pacifica-earn.js +58 -0
  275. package/dist/mcp-server.js +113 -59
  276. package/dist/pacifica/client.d.ts +67 -4
  277. package/dist/pacifica/client.js +170 -8
  278. package/dist/pacifica/rate-limit.d.ts +144 -0
  279. package/dist/pacifica/rate-limit.js +267 -0
  280. package/dist/pacifica/types/order.d.ts +2 -1
  281. package/dist/plan-executor.js +7 -2
  282. package/dist/rebalance.js +1 -1
  283. package/dist/risk.d.ts +2 -1
  284. package/dist/risk.js +2 -4
  285. package/dist/settings.d.ts +32 -3
  286. package/dist/signer/ows-evm.d.ts +1 -1
  287. package/dist/signer/ows-evm.js +1 -1
  288. package/dist/smart-order.d.ts +1 -1
  289. package/dist/smart-order.js +1 -1
  290. package/dist/strategies/funding-arb.d.ts +1 -1
  291. package/dist/strategies/funding-arb.js +1 -1
  292. package/dist/trade-validator.js +2 -2
  293. package/dist/utils.d.ts +12 -3
  294. package/dist/utils.js +11 -3
  295. package/dist/venue-health.d.ts +40 -0
  296. package/dist/venue-health.js +75 -0
  297. package/dist/withdraw-gate.d.ts +1 -1
  298. package/dist/withdraw-gate.js +1 -1
  299. package/dist/ws/index.d.ts +959 -0
  300. package/dist/ws/index.js +4295 -0
  301. package/dist/ws/node.d.ts +975 -0
  302. package/dist/ws/node.js +4366 -0
  303. package/dist/ws/pacifica-instrument.d.ts +8 -0
  304. package/dist/ws/pacifica-instrument.js +8 -0
  305. package/dist/ws-cli/credentials.d.ts +62 -0
  306. package/dist/ws-cli/credentials.js +126 -0
  307. package/package.json +58 -10
  308. package/skills/perp-cli/SKILL.md +62 -9
  309. package/skills/perp-cli/references/agent-operations.md +7 -0
  310. package/skills/perp-cli/references/commands.md +76 -5
  311. package/skills/perp-cli/references/ws-library.md +492 -0
@@ -0,0 +1,360 @@
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
+ // ── parsing ──
79
+ export function parseDeriveInstrument(raw) {
80
+ const o = obj(raw, "instrument");
81
+ const name = str(o, "instrument_name", "instrument");
82
+ const what = `instrument ${name}`;
83
+ if (str(o, "instrument_type", what) !== "option")
84
+ throw deriveShapeError(`${what} is not an option`);
85
+ const d = obj(o.option_details, `${what}.option_details`);
86
+ const t = str(d, "option_type", `${what}.option_details`);
87
+ if (t !== "C" && t !== "P")
88
+ throw deriveShapeError(`${what}.option_details.option_type "${t}" is not C / P`);
89
+ const expiryMs = int(d, "expiry", `${what}.option_details`) * 1000;
90
+ const isActive = o.is_active;
91
+ if (typeof isActive !== "boolean")
92
+ throw deriveShapeError(`${what}.is_active is not a boolean`);
93
+ return {
94
+ name,
95
+ underlying: str(o, "base_currency", what),
96
+ quoteCurrency: str(o, "quote_currency", what),
97
+ kind: t === "C" ? "call" : "put",
98
+ strike: dec(d, "strike", `${what}.option_details`),
99
+ expiryMs,
100
+ expiry: utcDate(expiryMs),
101
+ isActive,
102
+ activationMs: int(o, "scheduled_activation", what) * 1000,
103
+ };
104
+ }
105
+ /** A ticker_slim snapshot (get_ticker / get_tickers value) of `inst` → OptionQuote. */
106
+ export function parseDeriveOptionTicker(raw, inst) {
107
+ const what = `ticker ${inst.name}`;
108
+ const t = obj(raw, what);
109
+ const bidPx = dec(t, "b", what), bidSz = dec(t, "B", what);
110
+ const askPx = dec(t, "a", what), askSz = dec(t, "A", what);
111
+ const bid = Number(bidPx) === 0 ? null : { price: bidPx, size: bidSz };
112
+ const ask = Number(askPx) === 0 ? null : { price: askPx, size: askSz };
113
+ const s = obj(t.stats, `${what}.stats`);
114
+ const trades = int(s, "n", `${what}.stats`);
115
+ const pr = t.option_pricing;
116
+ let p = null;
117
+ if (pr !== null && pr !== undefined)
118
+ p = obj(pr, `${what}.option_pricing`);
119
+ const pw = `${what}.option_pricing`;
120
+ return {
121
+ schema: OPTIONS_SCHEMA,
122
+ venue: VENUE,
123
+ instrument: inst.name,
124
+ underlying: inst.underlying,
125
+ quoteCurrency: inst.quoteCurrency,
126
+ kind: inst.kind,
127
+ strike: inst.strike,
128
+ expiry: inst.expiry,
129
+ expiryMs: inst.expiryMs,
130
+ bid,
131
+ ask,
132
+ mark: dec(t, "M", what),
133
+ index: dec(t, "I", what),
134
+ forward: p === null ? null : dec(p, "f", pw),
135
+ discountFactor: p === null ? null : dec(p, "df", pw),
136
+ iv: {
137
+ mark: p === null ? null : nonZero(dec(p, "i", pw)),
138
+ bid: p === null || bid === null ? null : nonZero(dec(p, "bi", pw)),
139
+ ask: p === null || ask === null ? null : nonZero(dec(p, "ai", pw)),
140
+ },
141
+ greeks: p === null ? null : {
142
+ 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),
143
+ },
144
+ openInterest: dec(s, "oi", `${what}.stats`),
145
+ stats24h: {
146
+ contracts: dec(s, "c", `${what}.stats`),
147
+ notional: dec(s, "v", `${what}.stats`),
148
+ premium: dec(s, "pr", `${what}.stats`),
149
+ trades,
150
+ high: trades === 0 ? null : dec(s, "h", `${what}.stats`),
151
+ low: trades === 0 ? null : dec(s, "l", `${what}.stats`),
152
+ },
153
+ timestampMs: int(t, "t", what),
154
+ };
155
+ }
156
+ /** Distinct active expiries of `instruments`, soonest first. */
157
+ export function expiriesOf(instruments) {
158
+ const by = new Map();
159
+ for (const i of instruments) {
160
+ if (!i.isActive)
161
+ continue;
162
+ const e = by.get(i.expiryMs) ?? { strikes: new Set(), n: 0 };
163
+ e.strikes.add(i.strike);
164
+ e.n++;
165
+ by.set(i.expiryMs, e);
166
+ }
167
+ return [...by.entries()]
168
+ .sort((a, b) => a[0] - b[0])
169
+ .map(([ms, e]) => ({ expiry: utcDate(ms), expiryMs: ms, strikes: e.strikes.size, instruments: e.n }));
170
+ }
171
+ /** The `n` strikes closest to `price` (ties → lower strike), returned ascending. */
172
+ export function strikesAround(strikes, price, n) {
173
+ const p = Number(price);
174
+ const nearest = [...strikes].sort((a, b) => (Math.abs(Number(a) - p) - Math.abs(Number(b) - p)) || (Number(a) - Number(b)));
175
+ return nearest.slice(0, n).sort((a, b) => Number(a) - Number(b));
176
+ }
177
+ /**
178
+ * Chain for one expiry from its instruments and the `get_tickers` map. Rows come from the listed
179
+ * instruments; a listed instrument without a ticker leaves its side null and is named in
180
+ * `missingTickers`. Index / forward are those of the newest ticker (largest `t`) — one ticker's values,
181
+ * not an average over the expiry.
182
+ */
183
+ export function buildChain(underlying, expiry, selection, instruments, tickersRaw, strikesAroundAtm) {
184
+ const tickers = obj(tickersRaw, "get_tickers result.tickers");
185
+ const rowsByStrike = new Map();
186
+ const missingTickers = [];
187
+ let latest = null;
188
+ for (const inst of instruments) {
189
+ const row = rowsByStrike.get(inst.strike) ?? { strike: inst.strike, call: null, put: null };
190
+ rowsByStrike.set(inst.strike, row);
191
+ if (!(inst.name in tickers)) {
192
+ missingTickers.push(inst.name);
193
+ continue;
194
+ }
195
+ const q = parseDeriveOptionTicker(tickers[inst.name], inst);
196
+ row[inst.kind] = q;
197
+ if (latest === null || q.timestampMs > latest.timestampMs)
198
+ latest = q;
199
+ }
200
+ const strikes = [...rowsByStrike.keys()];
201
+ let atm = null;
202
+ if (latest !== null) {
203
+ const reference = latest.forward !== null ? "forward" : "index";
204
+ const price = latest.forward !== null ? latest.forward : latest.index;
205
+ atm = { strike: strikesAround(strikes, price, 1)[0], reference, price };
206
+ }
207
+ let keep = strikes;
208
+ if (strikesAroundAtm !== null) {
209
+ if (atm === null) {
210
+ throw new PerpError("EXCHANGE_ERROR", `Derive returned no ticker for ${underlying} ${expiry.expiry}; the ATM strike for --strikes is unknown`, {
211
+ exchange: VENUE, remediation: `Run without --strikes to see the listed strikes`,
212
+ });
213
+ }
214
+ keep = strikesAround(strikes, atm.price, strikesAroundAtm);
215
+ }
216
+ const rows = keep
217
+ .sort((a, b) => Number(a) - Number(b))
218
+ .map(k => rowsByStrike.get(k));
219
+ return {
220
+ schema: OPTIONS_SCHEMA,
221
+ venue: VENUE,
222
+ underlying,
223
+ expiry: expiry.expiry,
224
+ expiryMs: expiry.expiryMs,
225
+ expirySelection: selection,
226
+ index: latest === null ? null : latest.index,
227
+ forward: latest === null ? null : latest.forward,
228
+ atm,
229
+ strikesListed: strikes.length,
230
+ rows,
231
+ missingTickers: missingTickers.sort(),
232
+ };
233
+ }
234
+ // ── venue ──
235
+ function noOptions(underlying) {
236
+ return new PerpError("SYMBOL_NOT_FOUND", `Derive has no ${underlying} options`, {
237
+ exchange: VENUE, remediation: `List underlyings: perp -e ${VENUE} options underlyings`,
238
+ });
239
+ }
240
+ export class DeriveOptionsVenue {
241
+ client;
242
+ id = VENUE;
243
+ displayName = "Derive";
244
+ constructor(client) {
245
+ this.client = client;
246
+ }
247
+ static forNetwork(network) {
248
+ return new DeriveOptionsVenue(new DeriveClient(network));
249
+ }
250
+ async listUnderlyings() {
251
+ const raw = await this.client.call("get_all_currencies", {});
252
+ if (!Array.isArray(raw))
253
+ throw deriveShapeError("get_all_currencies result is not a list");
254
+ const underlyings = raw
255
+ .map((c, i) => obj(c, `currency #${i}`))
256
+ .filter(c => c.option !== null && c.option !== undefined)
257
+ .map(c => {
258
+ const what = `currency ${String(c.currency)}`;
259
+ return { underlying: str(c, "currency", what), spotPrice: c.spot_price === null ? null : dec(c, "spot_price", what) };
260
+ })
261
+ .sort((a, b) => a.underlying.localeCompare(b.underlying));
262
+ return { schema: OPTIONS_SCHEMA, venue: VENUE, underlyings };
263
+ }
264
+ /** Every non-expired option instrument of `underlying` (all pages). 12001 → SYMBOL_NOT_FOUND. */
265
+ async instruments(underlying) {
266
+ const out = [];
267
+ for (let page = 1, pages = 1; page <= pages; page++) {
268
+ let res;
269
+ try {
270
+ res = await this.client.call("get_all_instruments", {
271
+ instrument_type: "option", currency: underlying, expired: false, page_size: PAGE_SIZE, page,
272
+ });
273
+ }
274
+ catch (e) {
275
+ if (deriveRpcCode(e) === DERIVE_INSTRUMENT_NOT_FOUND)
276
+ throw noOptions(underlying);
277
+ throw e;
278
+ }
279
+ const r = obj(res, "get_all_instruments result");
280
+ if (!Array.isArray(r.instruments))
281
+ throw deriveShapeError("get_all_instruments result.instruments is not a list");
282
+ pages = int(obj(r.pagination, "get_all_instruments result.pagination"), "num_pages", "pagination");
283
+ for (const i of r.instruments)
284
+ out.push(parseDeriveInstrument(i));
285
+ }
286
+ return out;
287
+ }
288
+ async listExpiries(underlyingInput) {
289
+ const underlying = normalizeUnderlying(underlyingInput);
290
+ const expiries = expiriesOf(await this.instruments(underlying));
291
+ if (expiries.length === 0)
292
+ throw noOptions(underlying);
293
+ return { schema: OPTIONS_SCHEMA, venue: VENUE, underlying, expiries };
294
+ }
295
+ async getChain(q) {
296
+ const underlying = normalizeUnderlying(q.underlying);
297
+ const wanted = q.expiry === null ? null : normalizeExpiry(q.expiry);
298
+ if (q.strikesAroundAtm !== null && (!Number.isInteger(q.strikesAroundAtm) || q.strikesAroundAtm < 1)) {
299
+ throw new PerpError("INVALID_PARAMS", `--strikes must be a positive integer (got ${q.strikesAroundAtm})`, {
300
+ remediation: "Pass a positive integer, e.g. --strikes 10",
301
+ });
302
+ }
303
+ const all = await this.instruments(underlying);
304
+ const expiries = expiriesOf(all);
305
+ if (expiries.length === 0)
306
+ throw noOptions(underlying);
307
+ const expiry = wanted === null ? expiries[0] : expiries.find(e => e.expiry === wanted);
308
+ if (expiry === undefined) {
309
+ throw new PerpError("SYMBOL_NOT_FOUND", `Derive has no active ${underlying} option expiry ${wanted}`, {
310
+ exchange: VENUE,
311
+ remediation: `Active expiries: ${expiries.map(e => e.expiry).join(", ")} (perp -e ${VENUE} options expiries ${underlying})`,
312
+ });
313
+ }
314
+ const listed = all.filter(i => i.isActive && i.expiryMs === expiry.expiryMs);
315
+ const res = obj(await this.client.call("get_tickers", {
316
+ instrument_type: "option", currency: underlying, expiry_date: expiry.expiry.replace(/-/g, ""),
317
+ }), "get_tickers result");
318
+ return buildChain(underlying, expiry, wanted === null ? "nearest" : "requested", listed, res.tickers, q.strikesAroundAtm);
319
+ }
320
+ async getQuote(instrumentInput) {
321
+ const name = instrumentInput.trim().toUpperCase();
322
+ // Both reads are needed (instrument facts + live ticker); issued together, judged instrument first.
323
+ const [instR, tickR] = await Promise.allSettled([
324
+ this.client.call("get_instrument", { instrument_name: name }),
325
+ this.client.call("get_ticker", { instrument_name: name }),
326
+ ]);
327
+ if (instR.status === "rejected") {
328
+ if (deriveRpcCode(instR.reason) === DERIVE_INSTRUMENT_NOT_FOUND) {
329
+ throw new PerpError("SYMBOL_NOT_FOUND", `Derive has no instrument ${name}`, {
330
+ exchange: VENUE, remediation: `Find option names: perp -e ${VENUE} options chain <UNDERLYING>`,
331
+ });
332
+ }
333
+ throw instR.reason;
334
+ }
335
+ const o = obj(instR.value, "get_instrument result");
336
+ if (o.instrument_type !== "option") {
337
+ throw new PerpError("INVALID_PARAMS", `${name} is a Derive ${String(o.instrument_type)}, not an option`, {
338
+ exchange: VENUE, remediation: `Find option names: perp -e ${VENUE} options chain <UNDERLYING>`,
339
+ });
340
+ }
341
+ const inst = parseDeriveInstrument(o);
342
+ if (!inst.isActive) {
343
+ throw new PerpError("SYMBOL_NOT_FOUND", `Derive option ${name} is listed but not active (activation ${new Date(inst.activationMs).toISOString()})`, {
344
+ exchange: VENUE, remediation: `Active expiries: perp -e ${VENUE} options expiries ${inst.underlying}`,
345
+ });
346
+ }
347
+ if (tickR.status === "rejected") {
348
+ // An active instrument with no ticker (-32602 "Ticker not found for instrument", live): an
349
+ // activation / expiry race on Derive's side — transient, never the user's INVALID_PARAMS.
350
+ if (deriveRpcCode(tickR.reason) === DERIVE_INVALID_PARAMS) {
351
+ throw new PerpError("EXCHANGE_ERROR", `Derive has no live ticker for active option ${name} (${tickR.reason instanceof Error ? tickR.reason.message : String(tickR.reason)})`, {
352
+ exchange: VENUE, rpcCode: DERIVE_INVALID_PARAMS,
353
+ 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}`,
354
+ });
355
+ }
356
+ throw tickR.reason;
357
+ }
358
+ return parseDeriveOptionTicker(tickR.value, inst);
359
+ }
360
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Extended public-only adapter — official API, MARKET DATA ONLY in this version (venues/extended.ts).
3
+ *
4
+ * Market data (markets, orderbook, trades, funding history, candles) is implemented with the
5
+ * documented public endpoints (https://api.docs.extended.exchange/ "Public REST-API"). Every
6
+ * account / trading method refuses with CAPABILITY_UNSUPPORTED before any request — there is no
7
+ * Extended account, order or signing code.
8
+ *
9
+ * A symbol is resolved first (resolveExtendedMarketRemote: `?market=<name>` reads, the full listing
10
+ * only when no wire-name candidate matches), so an unlisted symbol is SYMBOL_NOT_FOUND before any
11
+ * per-market request (the orderbook of an unknown market answers `{"status":"OK"}` without data,
12
+ * its trades `[]` — live 2026-10-19).
13
+ */
14
+ import type { ExchangeAdapter, ExchangeBalance, ExchangeFundingPayment, ExchangeKline, ExchangeMarketInfo, ExchangeOrder, ExchangePosition, ExchangeTrade, ExchangePublicTrade } from "../interface.js";
15
+ import { type VenueDefinition } from "../venue.js";
16
+ import { type AssetClass, type Network } from "../venue-types.js";
17
+ /** CLI candle interval → Extended ISO-8601 duration (docs "Get candles history" / candles stream). */
18
+ export declare const EXTENDED_KLINE_INTERVALS: Readonly<Record<string, {
19
+ wire: string;
20
+ ms: number;
21
+ }>>;
22
+ /** Documented per-request maximum of the candles and funding-history endpoints. */
23
+ export declare const EXTENDED_MAX_RECORDS = 10000;
24
+ /** Legacy market row plus the Extended fields shown in JSON (asset class, precision, schedule). */
25
+ export type ExtendedMarketInfo = ExchangeMarketInfo & {
26
+ venueSymbol: string;
27
+ canonical: string | null;
28
+ assetClass: AssetClass;
29
+ tickSize: string;
30
+ minSize: string;
31
+ tradable: boolean;
32
+ status: "ACTIVE" | "REDUCE_ONLY";
33
+ isRfq: boolean;
34
+ tradingHours: string;
35
+ isOffHours: boolean;
36
+ /** `fundingRate` is the current hour's accruing rate (fraction per hour), applied at nextFundingTime. */
37
+ fundingRateKind: "accruing";
38
+ nextFundingTime: number | null;
39
+ };
40
+ /** One applied hourly funding print (fraction per hour) with its normalization. */
41
+ export interface ExtendedFundingPrint {
42
+ time: number;
43
+ rate: string;
44
+ price: null;
45
+ rateKind: "lastSettled";
46
+ intervalHours: 1;
47
+ /** rate × 24 × 365 × 100 (APR %, simple). */
48
+ annualizedPct: number;
49
+ }
50
+ export declare class ExtendedPublicAdapter implements ExchangeAdapter {
51
+ readonly venue: VenueDefinition;
52
+ readonly network: Network;
53
+ readonly name: "extended";
54
+ readonly aliases: readonly ["ext", "x10"];
55
+ private readonly client;
56
+ constructor(venue: VenueDefinition, network: Network);
57
+ /** Per-market reads resolve the symbol with the small `?market=` read (public.ts), not the full listing. */
58
+ private market;
59
+ getMarkets(): Promise<ExtendedMarketInfo[]>;
60
+ getOrderbook(symbol: string): Promise<{
61
+ bids: [string, string][];
62
+ asks: [string, string][];
63
+ }>;
64
+ getRecentTrades(symbol: string, limit?: number): Promise<ExchangePublicTrade[]>;
65
+ /** Applied (settled) hourly rates, newest first — fraction per hour; no oracle price published. */
66
+ getFundingHistory(symbol: string, limit?: number): Promise<ExtendedFundingPrint[]>;
67
+ getKlines(symbol: string, interval: string, startTime: number, endTime: number): Promise<ExchangeKline[]>;
68
+ private refuse;
69
+ getBalance(): Promise<ExchangeBalance>;
70
+ getPositions(): Promise<ExchangePosition[]>;
71
+ getOpenOrders(): Promise<ExchangeOrder[]>;
72
+ getOrderHistory(): Promise<ExchangeOrder[]>;
73
+ getTradeHistory(): Promise<ExchangeTrade[]>;
74
+ getFundingPayments(): Promise<ExchangeFundingPayment[]>;
75
+ marketOrder(): Promise<unknown>;
76
+ limitOrder(): Promise<unknown>;
77
+ editOrder(): Promise<unknown>;
78
+ cancelOrder(): Promise<unknown>;
79
+ cancelAllOrders(): Promise<unknown>;
80
+ setLeverage(): Promise<unknown>;
81
+ stopOrder(): Promise<unknown>;
82
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Extended public-only adapter — official API, MARKET DATA ONLY in this version (venues/extended.ts).
3
+ *
4
+ * Market data (markets, orderbook, trades, funding history, candles) is implemented with the
5
+ * documented public endpoints (https://api.docs.extended.exchange/ "Public REST-API"). Every
6
+ * account / trading method refuses with CAPABILITY_UNSUPPORTED before any request — there is no
7
+ * Extended account, order or signing code.
8
+ *
9
+ * A symbol is resolved first (resolveExtendedMarketRemote: `?market=<name>` reads, the full listing
10
+ * only when no wire-name candidate matches), so an unlisted symbol is SYMBOL_NOT_FOUND before any
11
+ * per-market request (the orderbook of an unknown market answers `{"status":"OK"}` without data,
12
+ * its trades `[]` — live 2026-10-19).
13
+ */
14
+ import { PerpError } from "../../errors.js";
15
+ import { annualizeHourlyRate } from "../../funding/normalize.js";
16
+ import { roleUnsupported } from "../venue.js";
17
+ import { toLegacyMarketInfo } from "../venue-types.js";
18
+ import { extendedShapeError } from "./client.js";
19
+ import { extendedClient, fetchExtendedMarketRows, resolveExtendedMarketRemote } from "./public.js";
20
+ /** CLI candle interval → Extended ISO-8601 duration (docs "Get candles history" / candles stream). */
21
+ export const EXTENDED_KLINE_INTERVALS = {
22
+ "1m": { wire: "PT1M", ms: 60_000 },
23
+ "5m": { wire: "PT5M", ms: 300_000 },
24
+ "15m": { wire: "PT15M", ms: 900_000 },
25
+ "30m": { wire: "PT30M", ms: 1_800_000 },
26
+ "1h": { wire: "PT1H", ms: 3_600_000 },
27
+ "2h": { wire: "PT2H", ms: 7_200_000 },
28
+ "4h": { wire: "PT4H", ms: 14_400_000 },
29
+ "8h": { wire: "PT8H", ms: 28_800_000 },
30
+ "12h": { wire: "PT12H", ms: 43_200_000 },
31
+ "1d": { wire: "PT24H", ms: 86_400_000 },
32
+ "1w": { wire: "P7D", ms: 604_800_000 },
33
+ };
34
+ /** Documented per-request maximum of the candles and funding-history endpoints. */
35
+ export const EXTENDED_MAX_RECORDS = 10_000;
36
+ const DEC = /^-?\d+(\.\d+)?$/;
37
+ function dec(v, what) {
38
+ if (typeof v !== "string" || !DEC.test(v))
39
+ throw extendedShapeError(`${what} is not a decimal string`);
40
+ return v;
41
+ }
42
+ function ms(v, what) {
43
+ if (typeof v !== "number" || !Number.isInteger(v) || v <= 0)
44
+ throw extendedShapeError(`${what} is not a ms timestamp`);
45
+ return v;
46
+ }
47
+ export class ExtendedPublicAdapter {
48
+ venue;
49
+ network;
50
+ name = "extended";
51
+ aliases = ["ext", "x10"];
52
+ client;
53
+ constructor(venue, network) {
54
+ this.venue = venue;
55
+ this.network = network;
56
+ this.client = extendedClient(network);
57
+ }
58
+ /** Per-market reads resolve the symbol with the small `?market=` read (public.ts), not the full listing. */
59
+ async market(symbol) {
60
+ return resolveExtendedMarketRemote(this.client, symbol);
61
+ }
62
+ // ── Market data ──
63
+ async getMarkets() {
64
+ const rows = await fetchExtendedMarketRows(this.client);
65
+ return rows.map(({ meta, market, nextFundingTime }) => ({
66
+ ...toLegacyMarketInfo(market),
67
+ venueSymbol: market.venueSymbol,
68
+ canonical: market.canonical,
69
+ assetClass: market.assetClass,
70
+ tickSize: market.tickSize,
71
+ minSize: market.minSize,
72
+ tradable: market.tradable,
73
+ status: meta.status,
74
+ isRfq: meta.isRfq,
75
+ tradingHours: meta.tradingHours,
76
+ isOffHours: meta.isOffHours,
77
+ fundingRateKind: "accruing",
78
+ nextFundingTime,
79
+ }));
80
+ }
81
+ async getOrderbook(symbol) {
82
+ const m = await this.market(symbol);
83
+ const body = await this.client.get(`/info/markets/${encodeURIComponent(m.name)}/orderbook`);
84
+ const data = body.data;
85
+ if (data === undefined || data === null || !Array.isArray(data.bid) || !Array.isArray(data.ask)) {
86
+ throw extendedShapeError(`orderbook of ${m.name} has no bid / ask lists`);
87
+ }
88
+ const side = (rows, what) => rows.map(r => {
89
+ const x = r;
90
+ return [dec(x.price, `${what} price`), dec(x.qty, `${what} qty`)];
91
+ });
92
+ return { bids: side(data.bid, "orderbook bid"), asks: side(data.ask, "orderbook ask") };
93
+ }
94
+ async getRecentTrades(symbol, limit = 50) {
95
+ const m = await this.market(symbol);
96
+ const body = await this.client.get(`/info/markets/${encodeURIComponent(m.name)}/trades`);
97
+ if (!Array.isArray(body.data))
98
+ throw extendedShapeError(`trades of ${m.name} is not a list`);
99
+ const trades = body.data.map(t => {
100
+ const x = t;
101
+ if (x.S !== "BUY" && x.S !== "SELL")
102
+ throw extendedShapeError(`trade side ${String(x.S)}`);
103
+ // Public tape carries no fee.
104
+ return { time: ms(x.T, "trade T"), symbol: m.symbol, side: x.S === "BUY" ? "buy" : "sell", price: dec(x.p, "trade p"), size: dec(x.q, "trade q"), fee: null };
105
+ });
106
+ // Newest first, like every other venue's tape.
107
+ return trades.sort((a, b) => b.time - a.time).slice(0, limit);
108
+ }
109
+ /** Applied (settled) hourly rates, newest first — fraction per hour; no oracle price published. */
110
+ async getFundingHistory(symbol, limit = 10) {
111
+ if (!Number.isInteger(limit) || limit < 1 || limit > EXTENDED_MAX_RECORDS) {
112
+ throw new PerpError("INVALID_PARAMS", `Extended: funding history limit must be 1..${EXTENDED_MAX_RECORDS} (got ${limit})`);
113
+ }
114
+ const m = await this.market(symbol);
115
+ const end = Date.now();
116
+ // One record per hour; two spare hours cover the in-progress hour and clock skew.
117
+ const start = end - (limit + 2) * 3_600_000;
118
+ const body = await this.client.get(`/info/${encodeURIComponent(m.name)}/funding`, [
119
+ ["startTime", String(start)], ["endTime", String(end)], ["limit", String(limit)],
120
+ ]);
121
+ if (!Array.isArray(body.data))
122
+ throw extendedShapeError(`funding history of ${m.name} is not a list`);
123
+ return body.data
124
+ .map(r => {
125
+ const x = r;
126
+ const rate = dec(x.f, "funding f");
127
+ return { time: ms(x.T, "funding T"), rate, price: null, rateKind: "lastSettled", intervalHours: 1, annualizedPct: annualizeHourlyRate(Number(rate)) };
128
+ })
129
+ .sort((a, b) => b.time - a.time)
130
+ .slice(0, limit);
131
+ }
132
+ async getKlines(symbol, interval, startTime, endTime) {
133
+ const iv = Object.prototype.hasOwnProperty.call(EXTENDED_KLINE_INTERVALS, interval) ? EXTENDED_KLINE_INTERVALS[interval] : undefined;
134
+ if (iv === undefined) {
135
+ throw new PerpError("INVALID_PARAMS", `Extended: unsupported candle interval "${interval}"`, {
136
+ remediation: `Use one of: ${Object.keys(EXTENDED_KLINE_INTERVALS).join(", ")}`,
137
+ });
138
+ }
139
+ if (!(endTime > startTime))
140
+ throw new PerpError("INVALID_PARAMS", "Extended: candle end time must be after the start time");
141
+ const m = await this.market(symbol);
142
+ const limit = Math.min(EXTENDED_MAX_RECORDS, Math.ceil((endTime - startTime) / iv.ms) + 1);
143
+ const body = await this.client.get(`/info/candles/${encodeURIComponent(m.name)}/trades`, [
144
+ ["interval", iv.wire], ["limit", String(limit)], ["endTime", String(endTime)],
145
+ ]);
146
+ if (!Array.isArray(body.data))
147
+ throw extendedShapeError(`candles of ${m.name} is not a list`);
148
+ return body.data
149
+ .map(c => {
150
+ const x = c;
151
+ return {
152
+ time: ms(x.T, "candle T"),
153
+ open: dec(x.o, "candle o"), high: dec(x.h, "candle h"), low: dec(x.l, "candle l"), close: dec(x.c, "candle c"),
154
+ volume: dec(x.v, "candle v"),
155
+ quoteVolume: null,
156
+ trades: null,
157
+ };
158
+ })
159
+ .filter(k => k.time >= startTime && k.time <= endTime)
160
+ .sort((a, b) => a.time - b.time);
161
+ }
162
+ // ── Account / trading: refused (market data only in this version) ──
163
+ refuse(feature) {
164
+ throw roleUnsupported(this.venue, feature);
165
+ }
166
+ async getBalance() { return this.refuse("account"); }
167
+ async getPositions() { return this.refuse("account"); }
168
+ async getOpenOrders() { return this.refuse("account"); }
169
+ async getOrderHistory() { return this.refuse("account"); }
170
+ async getTradeHistory() { return this.refuse("account"); }
171
+ async getFundingPayments() { return this.refuse("account"); }
172
+ async marketOrder() { return this.refuse("trading"); }
173
+ async limitOrder() { return this.refuse("trading"); }
174
+ async editOrder() { return this.refuse("trading"); }
175
+ async cancelOrder() { return this.refuse("trading"); }
176
+ async cancelAllOrders() { return this.refuse("trading"); }
177
+ async setLeverage() { return this.refuse("trading"); }
178
+ async stopOrder() { return this.refuse("trading"); }
179
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Extended public REST transport (official API, https://api.docs.extended.exchange/).
3
+ *
4
+ * Every response is `{status:"OK", data, pagination?}` or `{status:"ERROR", error:{code, message}}`
5
+ * (live 2026-10-19: an unknown market on /info/markets?market= or /info/<m>/funding is HTTP 400
6
+ * `{"code":1001,"message":"Market not found"}`). A User-Agent header is mandatory (docs
7
+ * "Mandatory headers"; without one the API answers 403). GET only — there is no write path.
8
+ */
9
+ import { PerpError } from "../../errors.js";
10
+ import type { RateLimitPolicy } from "../venue.js";
11
+ export declare const EXTENDED_HTTP_TIMEOUT_MS = 15000;
12
+ export declare const EXTENDED_USER_AGENT = "perp-cli";
13
+ /** Extended's error code for an unknown market (live 2026-10-19). */
14
+ export declare const EXTENDED_MARKET_NOT_FOUND = 1001;
15
+ /** A body that does not match the documented shape: an explicit EXCHANGE_ERROR, never a guess. */
16
+ export declare function extendedShapeError(what: string): PerpError;
17
+ export interface ExtendedErrorBody {
18
+ code: number | string | null;
19
+ message: string;
20
+ }
21
+ export declare class ExtendedClient {
22
+ private readonly restUrl;
23
+ private readonly rateLimit;
24
+ /** Limiter bucket: one per network (the documented limit is per IP, per host). */
25
+ private readonly bucket;
26
+ constructor(restUrl: string, rateLimit: RateLimitPolicy,
27
+ /** Limiter bucket: one per network (the documented limit is per IP, per host). */
28
+ bucket: string);
29
+ /**
30
+ * GET <rest><path>; returns the parsed body (`status: "OK"`). Every failure is a PerpError:
31
+ * transport → EXCHANGE_UNREACHABLE, 429 → RATE_LIMITED, `{status:"ERROR"}` / non-2xx /
32
+ * non-JSON → EXCHANGE_ERROR (an unknown market keeps the venue's 1001 code in details).
33
+ */
34
+ get<T = unknown>(path: string, query?: ReadonlyArray<[string, string]>): Promise<{
35
+ status: "OK";
36
+ data?: T;
37
+ } & Record<string, unknown>>;
38
+ }