perp-cli 0.14.3 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. package/README.ja.md +243 -0
  2. package/README.ko.md +243 -0
  3. package/README.md +138 -405
  4. package/README.zh.md +243 -0
  5. package/dist/agent-wallet/store.d.ts +3 -2
  6. package/dist/agent-wallet/store.js +2 -2
  7. package/dist/api/public/hyperliquid.js +4 -0
  8. package/dist/api/public/lighter.d.ts +5 -0
  9. package/dist/api/public/lighter.js +5 -0
  10. package/dist/api/public/pacifica.js +85 -15
  11. package/dist/api/public/urls.d.ts +2 -0
  12. package/dist/api/public/urls.js +3 -0
  13. package/dist/arb/sizing.js +4 -2
  14. package/dist/arb/state.d.ts +6 -0
  15. package/dist/arb/utils.d.ts +1 -2
  16. package/dist/arb/utils.js +1 -2
  17. package/dist/balance-by-address.d.ts +39 -0
  18. package/dist/balance-by-address.js +44 -0
  19. package/dist/bot/apex/pulse.js +3 -0
  20. package/dist/bot/apex/radar.js +2 -1
  21. package/dist/bot/engine.d.ts +10 -1
  22. package/dist/bot/engine.js +14 -6
  23. package/dist/bot/indicators.d.ts +7 -0
  24. package/dist/bot/indicators.js +16 -0
  25. package/dist/bot/perp-order.d.ts +4 -4
  26. package/dist/bot/perp-order.js +5 -5
  27. package/dist/bot/quarantine.d.ts +1 -1
  28. package/dist/bot/quarantine.js +1 -1
  29. package/dist/bot/quote-rules.js +1 -1
  30. package/dist/bot/strategies/apex-strategy.js +3 -2
  31. package/dist/bot/strategies/engine-mm.js +6 -5
  32. package/dist/bot/strategies/funding-arb-strategy.js +12 -5
  33. package/dist/bot/strategies/funding-arb-utils.d.ts +15 -3
  34. package/dist/bot/strategies/funding-arb-utils.js +53 -20
  35. package/dist/bot/strategies/funding-auto.js +2 -2
  36. package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
  37. package/dist/bot/strategies/liquidation-mm.js +8 -2
  38. package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
  39. package/dist/bot/strategies/momentum-breakout.js +11 -3
  40. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
  41. package/dist/bot/strategies/spot-perp-arb-strategy.js +6 -4
  42. package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
  43. package/dist/bot/strategies/spot-perp-executor.js +3 -3
  44. package/dist/bot/strategy-types.d.ts +2 -1
  45. package/dist/bot/tui/BotDashboard.d.ts +1 -1
  46. package/dist/bot/tui/BotDashboard.js +1 -1
  47. package/dist/cache.js +12 -6
  48. package/dist/cli-program.d.ts +15 -0
  49. package/dist/cli-program.js +126 -6
  50. package/dist/commands/account.js +168 -24
  51. package/dist/commands/agent-import.d.ts +7 -0
  52. package/dist/commands/agent-import.js +30 -1
  53. package/dist/commands/agent.d.ts +6 -3
  54. package/dist/commands/agent.js +160 -12
  55. package/dist/commands/arb/index.js +23 -81
  56. package/dist/commands/arb-auto.d.ts +24 -0
  57. package/dist/commands/arb-auto.js +175 -97
  58. package/dist/commands/arb.d.ts +20 -0
  59. package/dist/commands/arb.js +107 -163
  60. package/dist/commands/backtest.js +23 -21
  61. package/dist/commands/bot.js +1 -1
  62. package/dist/commands/builder.js +1 -1
  63. package/dist/commands/funds.js +113 -3
  64. package/dist/commands/init.d.ts +1 -1
  65. package/dist/commands/init.js +27 -7
  66. package/dist/commands/manage.js +23 -21
  67. package/dist/commands/market.d.ts +48 -0
  68. package/dist/commands/market.js +395 -100
  69. package/dist/commands/options.d.ts +14 -0
  70. package/dist/commands/options.js +151 -0
  71. package/dist/commands/pacifica-earn.d.ts +5 -0
  72. package/dist/commands/pacifica-earn.js +455 -0
  73. package/dist/commands/pacifica-orders.d.ts +14 -0
  74. package/dist/commands/pacifica-orders.js +142 -0
  75. package/dist/commands/portfolio.d.ts +16 -0
  76. package/dist/commands/portfolio.js +83 -55
  77. package/dist/commands/rebalance.d.ts +1 -0
  78. package/dist/commands/rebalance.js +15 -0
  79. package/dist/commands/risk.d.ts +11 -2
  80. package/dist/commands/risk.js +39 -44
  81. package/dist/commands/trade.d.ts +1 -0
  82. package/dist/commands/trade.js +67 -62
  83. package/dist/commands/wallet.js +1 -1
  84. package/dist/config.d.ts +2 -2
  85. package/dist/config.js +15 -4
  86. package/dist/constants.js +6 -0
  87. package/dist/cross-chain-margin.d.ts +3 -1
  88. package/dist/cross-chain-margin.js +4 -2
  89. package/dist/dashboard/server.js +3 -3
  90. package/dist/dashboard/ui.js +9 -6
  91. package/dist/errors.d.ts +29 -1
  92. package/dist/errors.js +35 -3
  93. package/dist/exchanges/address.d.ts +8 -0
  94. package/dist/exchanges/address.js +47 -0
  95. package/dist/exchanges/arcus/adapter.d.ts +16 -12
  96. package/dist/exchanges/arcus/adapter.js +45 -18
  97. package/dist/exchanges/arcus/agent.d.ts +1 -1
  98. package/dist/exchanges/arcus/agent.js +1 -1
  99. package/dist/exchanges/arcus/client.js +1 -1
  100. package/dist/exchanges/arcus/public.js +3 -2
  101. package/dist/exchanges/arcus/signing.d.ts +2 -3
  102. package/dist/exchanges/arcus/signing.js +1 -1
  103. package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
  104. package/dist/exchanges/arcus/spot-chain.js +1 -1
  105. package/dist/exchanges/arcus/spot-router.d.ts +1 -1
  106. package/dist/exchanges/arcus/spot-router.js +2 -2
  107. package/dist/exchanges/arcus/spot.d.ts +2 -3
  108. package/dist/exchanges/arcus/spot.js +2 -3
  109. package/dist/exchanges/aster-spot.d.ts +3 -3
  110. package/dist/exchanges/aster-spot.js +6 -9
  111. package/dist/exchanges/aster-typed-data.d.ts +1 -1
  112. package/dist/exchanges/aster-typed-data.js +1 -1
  113. package/dist/exchanges/aster.d.ts +30 -10
  114. package/dist/exchanges/aster.js +153 -53
  115. package/dist/exchanges/builder-fee.d.ts +1 -1
  116. package/dist/exchanges/builder-fee.js +13 -2
  117. package/dist/exchanges/canonical-symbols.d.ts +8 -2
  118. package/dist/exchanges/canonical-symbols.js +46 -2
  119. package/dist/exchanges/capabilities.d.ts +2 -0
  120. package/dist/exchanges/capabilities.js +2 -1
  121. package/dist/exchanges/derive/client.d.ts +37 -0
  122. package/dist/exchanges/derive/client.js +135 -0
  123. package/dist/exchanges/derive/options.d.ts +73 -0
  124. package/dist/exchanges/derive/options.js +379 -0
  125. package/dist/exchanges/extended/adapter.d.ts +96 -0
  126. package/dist/exchanges/extended/adapter.js +241 -0
  127. package/dist/exchanges/extended/client.d.ts +38 -0
  128. package/dist/exchanges/extended/client.js +88 -0
  129. package/dist/exchanges/extended/public.d.ts +133 -0
  130. package/dist/exchanges/extended/public.js +436 -0
  131. package/dist/exchanges/grvt/adapter.d.ts +100 -0
  132. package/dist/exchanges/grvt/adapter.js +283 -0
  133. package/dist/exchanges/grvt/client.d.ts +25 -0
  134. package/dist/exchanges/grvt/client.js +86 -0
  135. package/dist/exchanges/grvt/public.d.ts +151 -0
  136. package/dist/exchanges/grvt/public.js +438 -0
  137. package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
  138. package/dist/exchanges/hyperliquid-outcome.js +29 -1
  139. package/dist/exchanges/hyperliquid-spot.d.ts +9 -1
  140. package/dist/exchanges/hyperliquid-spot.js +24 -16
  141. package/dist/exchanges/hyperliquid.d.ts +199 -22
  142. package/dist/exchanges/hyperliquid.js +550 -202
  143. package/dist/exchanges/index.d.ts +1 -1
  144. package/dist/exchanges/index.js +1 -1
  145. package/dist/exchanges/interface.d.ts +157 -13
  146. package/dist/exchanges/interface.js +47 -0
  147. package/dist/exchanges/ioc-outcome.d.ts +1 -1
  148. package/dist/exchanges/ioc-outcome.js +1 -1
  149. package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
  150. package/dist/exchanges/legacy-trading-v2.js +7 -2
  151. package/dist/exchanges/lighter-public.d.ts +67 -12
  152. package/dist/exchanges/lighter-public.js +250 -82
  153. package/dist/exchanges/lighter-signer.d.ts +1 -1
  154. package/dist/exchanges/lighter-signer.js +1 -1
  155. package/dist/exchanges/lighter-spot.d.ts +10 -5
  156. package/dist/exchanges/lighter-spot.js +105 -97
  157. package/dist/exchanges/lighter.d.ts +80 -21
  158. package/dist/exchanges/lighter.js +312 -105
  159. package/dist/exchanges/ondo/adapter.d.ts +38 -13
  160. package/dist/exchanges/ondo/adapter.js +86 -27
  161. package/dist/exchanges/ondo/agent.d.ts +1 -1
  162. package/dist/exchanges/ondo/agent.js +1 -1
  163. package/dist/exchanges/ondo/client.d.ts +1 -1
  164. package/dist/exchanges/ondo/client.js +2 -2
  165. package/dist/exchanges/ondo/public.d.ts +35 -3
  166. package/dist/exchanges/ondo/public.js +75 -32
  167. package/dist/exchanges/ondo/spot.d.ts +2 -2
  168. package/dist/exchanges/ondo/spot.js +3 -3
  169. package/dist/exchanges/options-interface.d.ts +158 -0
  170. package/dist/exchanges/options-interface.js +17 -0
  171. package/dist/exchanges/options-venues.d.ts +12 -0
  172. package/dist/exchanges/options-venues.js +35 -0
  173. package/dist/exchanges/outcome-interface.d.ts +4 -0
  174. package/dist/exchanges/pacifica-atomic.d.ts +211 -0
  175. package/dist/exchanges/pacifica-atomic.js +440 -0
  176. package/dist/exchanges/pacifica-decimal.d.ts +10 -0
  177. package/dist/exchanges/pacifica-decimal.js +51 -0
  178. package/dist/exchanges/pacifica-presigned.d.ts +53 -0
  179. package/dist/exchanges/pacifica-presigned.js +141 -0
  180. package/dist/exchanges/pacifica-print.d.ts +200 -0
  181. package/dist/exchanges/pacifica-print.js +417 -0
  182. package/dist/exchanges/pacifica-spot.d.ts +8 -2
  183. package/dist/exchanges/pacifica-spot.js +26 -18
  184. package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
  185. package/dist/exchanges/pacifica-typed-data.js +1 -1
  186. package/dist/exchanges/pacifica-vaults.d.ts +130 -0
  187. package/dist/exchanges/pacifica-vaults.js +277 -0
  188. package/dist/exchanges/pacifica.d.ts +108 -10
  189. package/dist/exchanges/pacifica.js +428 -171
  190. package/dist/exchanges/precision.d.ts +3 -1
  191. package/dist/exchanges/precision.js +9 -2
  192. package/dist/exchanges/public-balances.d.ts +25 -0
  193. package/dist/exchanges/public-balances.js +158 -0
  194. package/dist/exchanges/public-positions.d.ts +43 -0
  195. package/dist/exchanges/public-positions.js +154 -0
  196. package/dist/exchanges/qfex/adapter.d.ts +69 -13
  197. package/dist/exchanges/qfex/adapter.js +102 -17
  198. package/dist/exchanges/qfex/agent.d.ts +1 -1
  199. package/dist/exchanges/qfex/agent.js +1 -1
  200. package/dist/exchanges/qfex/client.d.ts +1 -1
  201. package/dist/exchanges/qfex/client.js +1 -1
  202. package/dist/exchanges/qfex/errors.d.ts +1 -1
  203. package/dist/exchanges/qfex/errors.js +2 -2
  204. package/dist/exchanges/qfex/public.d.ts +65 -12
  205. package/dist/exchanges/qfex/public.js +131 -25
  206. package/dist/exchanges/qfex/signing.d.ts +2 -2
  207. package/dist/exchanges/qfex/signing.js +4 -5
  208. package/dist/exchanges/qfex/ws-transport.d.ts +3 -3
  209. package/dist/exchanges/qfex/ws-transport.js +3 -3
  210. package/dist/exchanges/rate-limiter.d.ts +1 -1
  211. package/dist/exchanges/rate-limiter.js +5 -2
  212. package/dist/exchanges/registry.d.ts +1 -1
  213. package/dist/exchanges/registry.js +1 -1
  214. package/dist/exchanges/risex/adapter.d.ts +160 -0
  215. package/dist/exchanges/risex/adapter.js +842 -0
  216. package/dist/exchanges/risex/agent.d.ts +154 -0
  217. package/dist/exchanges/risex/agent.js +384 -0
  218. package/dist/exchanges/risex/client.d.ts +47 -0
  219. package/dist/exchanges/risex/client.js +174 -0
  220. package/dist/exchanges/risex/nonce.d.ts +43 -0
  221. package/dist/exchanges/risex/nonce.js +212 -0
  222. package/dist/exchanges/risex/public.d.ts +64 -0
  223. package/dist/exchanges/risex/public.js +150 -0
  224. package/dist/exchanges/risex/session.d.ts +71 -0
  225. package/dist/exchanges/risex/session.js +171 -0
  226. package/dist/exchanges/risex/typed-data.d.ts +173 -0
  227. package/dist/exchanges/risex/typed-data.js +240 -0
  228. package/dist/exchanges/risex/units.d.ts +29 -0
  229. package/dist/exchanges/risex/units.js +104 -0
  230. package/dist/exchanges/slippage.d.ts +1 -1
  231. package/dist/exchanges/slippage.js +1 -1
  232. package/dist/exchanges/spot-factory.d.ts +3 -3
  233. package/dist/exchanges/spot-factory.js +8 -8
  234. package/dist/exchanges/spot-interface.d.ts +5 -5
  235. package/dist/exchanges/spot-interface.js +2 -2
  236. package/dist/exchanges/spot-result.d.ts +3 -3
  237. package/dist/exchanges/spot-result.js +3 -3
  238. package/dist/exchanges/trading-v2.d.ts +3 -3
  239. package/dist/exchanges/trading-v2.js +9 -6
  240. package/dist/exchanges/txflow/adapter.d.ts +61 -0
  241. package/dist/exchanges/txflow/adapter.js +150 -0
  242. package/dist/exchanges/txflow/client.d.ts +47 -0
  243. package/dist/exchanges/txflow/client.js +155 -0
  244. package/dist/exchanges/txflow/public.d.ts +87 -0
  245. package/dist/exchanges/txflow/public.js +355 -0
  246. package/dist/exchanges/unknown-symbol.d.ts +34 -0
  247. package/dist/exchanges/unknown-symbol.js +56 -0
  248. package/dist/exchanges/variational/adapter.d.ts +67 -0
  249. package/dist/exchanges/variational/adapter.js +137 -0
  250. package/dist/exchanges/variational/client.d.ts +21 -0
  251. package/dist/exchanges/variational/client.js +66 -0
  252. package/dist/exchanges/variational/public.d.ts +126 -0
  253. package/dist/exchanges/variational/public.js +430 -0
  254. package/dist/exchanges/venue-types.d.ts +52 -10
  255. package/dist/exchanges/venue-types.js +82 -11
  256. package/dist/exchanges/venue.d.ts +150 -11
  257. package/dist/exchanges/venue.js +126 -4
  258. package/dist/exchanges/venues/arcus.d.ts +1 -2
  259. package/dist/exchanges/venues/arcus.js +5 -4
  260. package/dist/exchanges/venues/aster.d.ts +2 -2
  261. package/dist/exchanges/venues/aster.js +6 -4
  262. package/dist/exchanges/venues/create-helpers.d.ts +1 -1
  263. package/dist/exchanges/venues/create-helpers.js +3 -3
  264. package/dist/exchanges/venues/extended.d.ts +21 -0
  265. package/dist/exchanges/venues/extended.js +96 -0
  266. package/dist/exchanges/venues/grvt.d.ts +30 -0
  267. package/dist/exchanges/venues/grvt.js +98 -0
  268. package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
  269. package/dist/exchanges/venues/hyperliquid.js +19 -8
  270. package/dist/exchanges/venues/index.d.ts +7 -2
  271. package/dist/exchanges/venues/index.js +8 -2
  272. package/dist/exchanges/venues/lighter-family.d.ts +7 -9
  273. package/dist/exchanges/venues/lighter-family.js +9 -11
  274. package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
  275. package/dist/exchanges/venues/lighter-rh.js +20 -8
  276. package/dist/exchanges/venues/lighter.d.ts +1 -1
  277. package/dist/exchanges/venues/lighter.js +18 -6
  278. package/dist/exchanges/venues/ondo.d.ts +1 -1
  279. package/dist/exchanges/venues/ondo.js +5 -3
  280. package/dist/exchanges/venues/pacifica.d.ts +2 -2
  281. package/dist/exchanges/venues/pacifica.js +15 -4
  282. package/dist/exchanges/venues/qfex.d.ts +1 -1
  283. package/dist/exchanges/venues/qfex.js +4 -2
  284. package/dist/exchanges/venues/risex-release.d.ts +14 -0
  285. package/dist/exchanges/venues/risex-release.js +14 -0
  286. package/dist/exchanges/venues/risex.d.ts +31 -0
  287. package/dist/exchanges/venues/risex.js +131 -0
  288. package/dist/exchanges/venues/txflow.d.ts +18 -0
  289. package/dist/exchanges/venues/txflow.js +77 -0
  290. package/dist/exchanges/venues/variational.d.ts +21 -0
  291. package/dist/exchanges/venues/variational.js +85 -0
  292. package/dist/execution-log.d.ts +1 -1
  293. package/dist/funding/legacy-public.d.ts +6 -4
  294. package/dist/funding/legacy-public.js +42 -41
  295. package/dist/funding/normalize.d.ts +1 -1
  296. package/dist/funding/normalize.js +1 -1
  297. package/dist/funding/rates.d.ts +95 -6
  298. package/dist/funding/rates.js +99 -19
  299. package/dist/index.js +7 -6
  300. package/dist/jobs.d.ts +1 -0
  301. package/dist/jobs.js +25 -1
  302. package/dist/landing.d.ts +2 -0
  303. package/dist/landing.js +1 -1
  304. package/dist/liquidity.d.ts +3 -1
  305. package/dist/liquidity.js +8 -4
  306. package/dist/mcp-pacifica-earn.d.ts +9 -0
  307. package/dist/mcp-pacifica-earn.js +64 -0
  308. package/dist/mcp-server.js +211 -68
  309. package/dist/pacifica/client.d.ts +69 -5
  310. package/dist/pacifica/client.js +181 -8
  311. package/dist/pacifica/rate-limit.d.ts +144 -0
  312. package/dist/pacifica/rate-limit.js +267 -0
  313. package/dist/pacifica/types/order.d.ts +2 -1
  314. package/dist/plan-executor.js +7 -2
  315. package/dist/positions-by-address.d.ts +34 -0
  316. package/dist/positions-by-address.js +46 -0
  317. package/dist/rebalance.js +1 -1
  318. package/dist/risk.d.ts +2 -1
  319. package/dist/risk.js +2 -4
  320. package/dist/settings.d.ts +32 -3
  321. package/dist/signer/ows-evm.d.ts +1 -1
  322. package/dist/signer/ows-evm.js +1 -1
  323. package/dist/smart-order.d.ts +1 -1
  324. package/dist/smart-order.js +1 -1
  325. package/dist/strategies/funding-arb.d.ts +1 -1
  326. package/dist/strategies/funding-arb.js +36 -63
  327. package/dist/trade-validator.js +2 -2
  328. package/dist/utils.d.ts +22 -3
  329. package/dist/utils.js +11 -3
  330. package/dist/venue-health.d.ts +40 -0
  331. package/dist/venue-health.js +75 -0
  332. package/dist/withdraw-gate.d.ts +1 -1
  333. package/dist/withdraw-gate.js +1 -1
  334. package/dist/ws/arcus-market.d.ts +9 -0
  335. package/dist/ws/arcus-market.js +9 -0
  336. package/dist/ws/aster-market.d.ts +33 -0
  337. package/dist/ws/aster-market.js +30 -0
  338. package/dist/ws/index.d.ts +980 -0
  339. package/dist/ws/index.js +4460 -0
  340. package/dist/ws/lighter-market.d.ts +14 -0
  341. package/dist/ws/lighter-market.js +13 -0
  342. package/dist/ws/node.d.ts +996 -0
  343. package/dist/ws/node.js +4554 -0
  344. package/dist/ws/pacifica-instrument.d.ts +8 -0
  345. package/dist/ws/pacifica-instrument.js +8 -0
  346. package/dist/ws/unclassified.d.ts +16 -0
  347. package/dist/ws/unclassified.js +43 -0
  348. package/dist/ws-cli/credentials.d.ts +62 -0
  349. package/dist/ws-cli/credentials.js +126 -0
  350. package/package.json +71 -10
  351. package/skills/perp-cli/SKILL.md +153 -36
  352. package/skills/perp-cli/package.json +15 -2
  353. package/skills/perp-cli/references/agent-operations.md +22 -12
  354. package/skills/perp-cli/references/commands.md +143 -17
  355. package/skills/perp-cli/references/strategies.md +38 -3
  356. package/skills/perp-cli/references/ws-library.md +523 -0
  357. package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
  358. package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
  359. package/skills/perp-cli/scripts/preflight.sh +27 -19
  360. package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
  361. package/skills/perp-cli/scripts/validate-arb.sh +56 -39
  362. package/dist/cli-spec.d.ts +0 -46
  363. package/dist/cli-spec.js +0 -81
@@ -1,12 +1,14 @@
1
+ import { assertCandleInterval } from "./interface.js";
2
+ import { isLighterMarketLive } from "../ws/lighter-market.js";
1
3
  import { alignProtectionPrice, floorSize, formatComputedPrice, mulDecimal, slippagePrice, toUnitsExact, unitsToString } from "./precision.js";
2
4
  import { unsupported } from "./trading-v2.js";
3
5
  import { LocalEvmSigner } from "../signer/index.js";
4
6
  import { isExpired } from "../agent-wallet/expiry.js";
5
- import { PerpError, classifyError, extractErrorMessage, orderNotSent, orderOutcomeUnknown, sendFailure, unknownActionRemediation, parsePositiveDecimal, DEFINITE_ORDER_FAILURE_CODES } from "../errors.js";
7
+ import { PerpError, classifyError, extractErrorMessage, orderNotSent, orderOutcomeUnknown, sendFailure, unknownActionRemediation, parsePositiveDecimal, DEFINITE_ORDER_FAILURE_CODES, noAccountKeyError } from "../errors.js";
6
8
  import { parseFiniteVenueNumber, venueRateField } from "../utils/numeric.js";
7
9
  import { lighterFamilyNetworks, LIGHTER_RH_RATE_LIMIT, LIGHTER_RH_RESERVED_KEY_INDEXES, LIGHTER_GTT_EXPIRY_MS, LIGHTER_CLIENT_ORDER_INDEX_MAX, LIGHTER_REFERENCE_PRICE_SOURCE, } from "./venues/lighter-family.js";
8
10
  import { acquireRate, rateLimitedError } from "./rate-limiter.js";
9
- import { lighterWafChallenge } from "./lighter-public.js";
11
+ import { fetchLighterFundingStats, lighterFundingStatsWarning, LIGHTER_BOOK_ORDERS_LIMIT, lighterBook, lighterWafChallenge } from "./lighter-public.js";
10
12
  import { fetchAndCache, invalidateCache, setCached, withCache, TTL_ACCOUNT } from "../cache.js";
11
13
  import { LighterWasmSigner } from "./lighter-signer.js";
12
14
  /** Lighter trigger orderType enum (matches WASM signer). */
@@ -35,12 +37,35 @@ export function classifyTriggerOrderType(side, triggerPrice, markPrice, isMarket
35
37
  }
36
38
  /** Lighter order time-in-force enum (lighter-go / SDK `TimeInForce`): 0 IOC, 1 GTT, 2 post-only. */
37
39
  export const LIGHTER_TIF = { IOC: 0, GTT: 1, POST_ONLY: 2 };
40
+ /**
41
+ * One GET /api/v1/fundings row → a funding history row (fraction per 1 h, signed).
42
+ * `rate` is percent per hour, unsigned; `direction` "long" = longs pay (positive), "short" = shorts pay.
43
+ */
44
+ export function lighterFundingRow(venue, f) {
45
+ const x = f;
46
+ if (typeof x.timestamp !== "number" || !Number.isFinite(x.timestamp) || x.timestamp <= 0) {
47
+ throw new PerpError("EXCHANGE_ERROR", `${venue} /fundings: row without a numeric timestamp: ${JSON.stringify(f).slice(0, 160)}`, { exchange: venue });
48
+ }
49
+ if (typeof x.rate !== "string" || !/^\d+(\.\d+)?$/.test(x.rate)) {
50
+ throw new PerpError("EXCHANGE_ERROR", `${venue} /fundings: row without an unsigned decimal rate: ${JSON.stringify(f).slice(0, 160)}`, { exchange: venue });
51
+ }
52
+ if (x.direction !== "long" && x.direction !== "short") {
53
+ throw new PerpError("EXCHANGE_ERROR", `${venue} /fundings: unknown direction ${JSON.stringify(x.direction)}`, { exchange: venue });
54
+ }
55
+ return {
56
+ time: x.timestamp * 1000,
57
+ rate: mulDecimal(x.rate, x.direction === "long" ? "0.01" : "-0.01"),
58
+ price: null,
59
+ intervalHours: 1,
60
+ rateKind: "lastSettled",
61
+ };
62
+ }
38
63
  /** Decimal step for `decimals` digits ("1" for 0, "0.01" for 2). */
39
64
  function lighterStep(decimals) {
40
65
  return decimals <= 0 ? "1" : `0.${"0".repeat(decimals - 1)}1`;
41
66
  }
42
67
  /**
43
- * CLI format rule for a Lighter client order id (§13.10 #9): the venue's
68
+ * CLI format rule for a Lighter client order id: the venue's
44
69
  * `clientOrderIndex` is numeric, so the id must be a plain decimal integer string
45
70
  * (digits only, no sign / leading zero / exponent) in 1 ≤ n ≤ 2^48 − 1.
46
71
  * Anything else → INVALID_PARAMS before anything is sent.
@@ -369,6 +394,65 @@ export function lighterRowCreatedMs(row) {
369
394
  * leverage = floor(10000 / bps), which returns the integer leverage that was set. margin_mode:
370
395
  * 0 cross, 1 isolated (the values updateLeverage signs). Anything else throws — never a guess.
371
396
  */
397
+ /**
398
+ * Balance of one Lighter-family account object (GET /account) — the one implementation behind
399
+ * LighterAdapter.getBalance() and the keyless `account balance --address`. Equity includes the
400
+ * spot collateral asset (USDC on Lighter, USDG on Lighter RH; separate from perp collateral).
401
+ */
402
+ export function lighterBalanceOf(acct, venue, collateralSymbol) {
403
+ const totalAsset = parseFiniteVenueNumber(acct.total_asset_value, "total_asset_value", "lighter");
404
+ const available = parseFiniteVenueNumber(acct.available_balance, "available_balance", "lighter");
405
+ const collateral = parseFiniteVenueNumber(acct.collateral, "collateral", "lighter");
406
+ const unrealizedPnl = acct.positions?.reduce((sum, p) => sum + parseFiniteVenueNumber(p.unrealized_pnl, "position.unrealized_pnl", "lighter"), 0) ?? 0;
407
+ // Include spot collateral balance (USDC on Lighter, USDG on Lighter RH; separate from perp collateral)
408
+ // An account without that asset holds none of it (0); an unreadable assets list / balance throws.
409
+ let spotUsdc = 0;
410
+ if (!Array.isArray(acct.assets)) {
411
+ throw new PerpError("EXCHANGE_ERROR", `${venue} account has no assets list — spot ${collateralSymbol} balance unknown`, { exchange: venue });
412
+ }
413
+ const usdcAsset = acct.assets.find(a => a?.symbol === collateralSymbol);
414
+ if (usdcAsset !== undefined)
415
+ spotUsdc = parseFiniteVenueNumber(usdcAsset.balance, `assets.${collateralSymbol}.balance`, "lighter");
416
+ return {
417
+ equity: String(totalAsset + spotUsdc),
418
+ available: String(available),
419
+ marginUsed: String(Math.max(0, collateral - available)),
420
+ unrealizedPnl: String(unrealizedPnl),
421
+ };
422
+ }
423
+ /**
424
+ * Open positions of one Lighter account (`GET /account` `accounts[].positions[]`): the venue lists
425
+ * every market the account ever traded or configured, so rows with `position` 0 are dropped. The
426
+ * ONE mapper of the keyed `getPositions` and the public-address read; `priceDecimalsOf` formats
427
+ * the computed mark (position_value / size) to the market's price decimals when known.
428
+ */
429
+ export function lighterPositionRows(positions, venue, priceDecimalsOf) {
430
+ return (positions ?? [])
431
+ .filter((p) => parseFiniteVenueNumber(p.position, "position", "lighter") !== 0)
432
+ .map((p) => {
433
+ const posSize = parseFiniteVenueNumber(p.position, "position", "lighter");
434
+ const positionValue = parseFiniteVenueNumber(p.position_value, "position_value", "lighter");
435
+ return {
436
+ symbol: String(p.symbol || `Market-${p.market_id}`),
437
+ side: (Number(p.sign) > 0 ? "long" : "short"),
438
+ size: String(Math.abs(posSize)),
439
+ entryPrice: String(p.avg_entry_price || "0"),
440
+ markPrice: (() => {
441
+ if (posSize === 0)
442
+ return "0";
443
+ const rawMark = positionValue / Math.abs(posSize);
444
+ const priceDec = priceDecimalsOf(String(p.symbol || ""));
445
+ return priceDec !== undefined ? rawMark.toFixed(priceDec) : formatComputedPrice(rawMark);
446
+ })(),
447
+ // The venue reports "0" when the position has no liquidation price (e.g. cross margin
448
+ // well covered): that is "none", not a price.
449
+ liquidationPrice: Number(p.liquidation_price) > 0 ? String(p.liquidation_price) : null,
450
+ unrealizedPnl: String(p.unrealized_pnl || "0"),
451
+ // The leverage CONFIGURED for the market (what `trade leverage` set) — not notional / equity.
452
+ ...lighterPositionSettings(p, venue),
453
+ };
454
+ });
455
+ }
372
456
  export function lighterPositionSettings(p, venue) {
373
457
  const pct = typeof p.initial_margin_fraction === "string" && /^\d+(\.\d+)?$/.test(p.initial_margin_fraction) ? Number(p.initial_margin_fraction) : NaN;
374
458
  const bps = Math.round(pct * 100);
@@ -409,8 +493,27 @@ export function lighterSelectAccount(accounts, accountIndex, venue) {
409
493
  * window is unknown (SYMBOL_NOT_FOUND) until the entry expires.
410
494
  */
411
495
  export const LIGHTER_MARKET_LIST_TTL_MS = 5 * 60_000;
496
+ /** GET /api/v1/candles resolutions, live-verified 2026-10-19 on lighter + lighter-rh mainnet and lighter testnet (3m / 2h / 6h / 8h / 1w / 1M / 2m: code 20001 "invalid param"). */
497
+ export const LIGHTER_CANDLE_INTERVALS = {
498
+ "1m": 60_000, "5m": 300_000, "15m": 900_000, "30m": 1_800_000,
499
+ "1h": 3_600_000, "4h": 14_400_000, "12h": 43_200_000, "1d": 86_400_000,
500
+ };
501
+ /** "Returns at most 500 candles per call" (apidocs.lighter.xyz/reference/candles). */
502
+ export const LIGHTER_CANDLES_PER_CALL = 500;
503
+ /** Max `limit` of GET /api/v1/recentTrades. */
504
+ export const LIGHTER_RECENT_TRADES_MAX = 100;
505
+ /** Requests one `market kline` may send (the venue allows 60 / minute on standard accounts). */
506
+ export const LIGHTER_KLINE_MAX_PAGES = 10;
507
+ /** A candle volume field: a finite number / numeric string → its string form; absent or non-numeric → null (never "" / "undefined"). */
508
+ function candleNumber(v) {
509
+ if (typeof v === "number" && Number.isFinite(v))
510
+ return String(v);
511
+ if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v)))
512
+ return v;
513
+ return null;
514
+ }
412
515
  export class LighterAdapter {
413
- /** GTC→GTT resting-order lifetime (28 days) for the execution record (§13.10 #14). */
516
+ /** GTC→GTT resting-order lifetime (28 days) for the execution record. */
414
517
  static gttExpiryMs = LIGHTER_GTT_EXPIRY_MS;
415
518
  /** Venue id ("lighter" | "lighter-rh"). */
416
519
  name;
@@ -434,6 +537,8 @@ export class LighterAdapter {
434
537
  _address;
435
538
  _marketMap = new Map(); // symbol → marketIndex
436
539
  _marketDecimals = new Map(); // symbol → decimals
540
+ /** symbol → live (isLighterMarketLive). Inactive markets stay in _marketMap for reads and closes. */
541
+ _marketLive = new Map();
437
542
  _evmKey;
438
543
  _apiKey;
439
544
  _accountIndexInit;
@@ -679,7 +784,7 @@ export class LighterAdapter {
679
784
  return { tier: "master" };
680
785
  if (this._evmKey)
681
786
  return { tier: "pk" };
682
- // Lighter RH master key is OWS-only (design §10.3): no env-key remediation for it.
787
+ // Lighter RH master key is OWS-only: no env-key remediation for it.
683
788
  const remediation = this.venueId === "lighter"
684
789
  ? "Run one of: (a) perp wallet agent approve lighter --master <wallet>; (b) perp wallet generate && perp -e lighter --ows <wallet>; (c) export LIGHTER_PRIVATE_KEY=0x..."
685
790
  : `Run one of: (a) perp wallet agent approve ${this.name} --master <wallet>; (b) perp wallet generate && perp -e ${this.name} --ows <wallet>`;
@@ -834,7 +939,8 @@ export class LighterAdapter {
834
939
  * the 60 / minute budget.
835
940
  */
836
941
  async _refreshMarketMap() {
837
- const perps = await withCache(`mkt:${this.venueId}:${this.networkName}:orderBooks`, LIGHTER_MARKET_LIST_TTL_MS, async () => {
942
+ // v2: rows carry `live` (entries cached before it lack the field).
943
+ const perps = await withCache(`mkt:${this.venueId}:${this.networkName}:orderBooks:v2`, LIGHTER_MARKET_LIST_TTL_MS, async () => {
838
944
  const res = await this.restGet("/orderBooks", {});
839
945
  if (!Array.isArray(res.order_books) || res.order_books.length === 0) {
840
946
  throw new PerpError("EXCHANGE_ERROR", `${this.name} orderBooks returned no order_books list — the market list is unknown`, {
@@ -851,19 +957,33 @@ export class LighterAdapter {
851
957
  if (typeof d.symbol !== "string" || !Number.isInteger(d.market_id) || !Number.isInteger(d.supported_size_decimals) || !Number.isInteger(d.supported_price_decimals)) {
852
958
  throw new PerpError("EXCHANGE_ERROR", `${this.name} orderBooks: perp market row without symbol / market_id / decimals: ${JSON.stringify(d).slice(0, 160)}`, { exchange: this.name });
853
959
  }
854
- rows.push({ symbol: d.symbol.toUpperCase(), marketId: d.market_id, size: d.supported_size_decimals, price: d.supported_price_decimals });
960
+ rows.push({ symbol: d.symbol.toUpperCase(), marketId: d.market_id, size: d.supported_size_decimals, price: d.supported_price_decimals, live: isLighterMarketLive(d) });
855
961
  }
856
962
  return rows;
857
963
  });
858
964
  for (const m of perps) {
859
965
  this._marketMap.set(m.symbol, m.marketId);
860
966
  this._marketDecimals.set(m.symbol, { size: m.size, price: m.price });
967
+ (this._marketLive ??= new Map()).set(m.symbol, m.live);
861
968
  }
862
969
  }
970
+ /**
971
+ * An order that can open or add exposure (not reduce-only) on a market that is not live
972
+ * (`status` inactive / `is_frozen`) is refused before signing. Reduce-only orders and closes are
973
+ * still sent, so a position on a market that went inactive can be closed.
974
+ */
975
+ assertOpenable(symbol, reduceOnly) {
976
+ if (reduceOnly || this._marketLive?.get(symbol.toUpperCase()) !== false)
977
+ return;
978
+ throw new PerpError("ORDER_REJECTED", `${this.name} market ${symbol.toUpperCase()} is inactive (status not "active" or frozen) — only reduce-only orders and closes are sent; nothing was signed`, {
979
+ exchange: this.name, marketInactive: true,
980
+ remediation: `Close or reduce with --reduce-only; list live markets: perp -e ${this.name} market list`,
981
+ });
982
+ }
863
983
  getMarketIndex(symbol) {
864
984
  const idx = this._marketMap.get(symbol.toUpperCase());
865
985
  if (idx === undefined)
866
- throw new PerpError("SYMBOL_NOT_FOUND", `Unknown Lighter market: ${symbol}`, { exchange: this.name });
986
+ throw new PerpError("SYMBOL_NOT_FOUND", `Unknown Lighter market: ${symbol}`, { exchange: this.name, remediation: `List markets: perp -e ${this.name} market list` });
867
987
  return idx;
868
988
  }
869
989
  /**
@@ -976,7 +1096,7 @@ export class LighterAdapter {
976
1096
  return last;
977
1097
  }
978
1098
  /**
979
- * Definite IOC outcome (§13.10 #4) from the executed tx: poll /tx by hash. A taker order the
1099
+ * Definite IOC outcome from the executed tx: poll /tx by hash. A taker order the
980
1100
  * venue reports "filled" (event_info `to.st` 3, `is` = sent) filled the whole order. Any other
981
1101
  * status (the remainder was cancelled — the venue zeroes `rs`, so is − rs is NOT a fill) →
982
1102
  * the venue's own `filled_base_amount` from accountOrders by client order index. Zero fill →
@@ -1117,7 +1237,7 @@ export class LighterAdapter {
1117
1237
  return null;
1118
1238
  }
1119
1239
  /**
1120
- * Final outcome of a placed resting / post-only order (§13.10 #4 analogue for non-IOC): poll
1240
+ * Final outcome of a placed resting / post-only order (analogue for non-IOC): poll
1121
1241
  * /tx (rejected → ORDER_REJECTED). Our order (`to`, matched by client order index) reported
1122
1242
  * "filled" in event_info → filled = is (the whole order). Any other status — open, or a
1123
1243
  * canceled-* whose remainder the venue zeroed (`rs` 0, so is − rs is NOT a fill) — or an
@@ -1196,28 +1316,44 @@ export class LighterAdapter {
1196
1316
  }
1197
1317
  return { baseAmount, priceTicks };
1198
1318
  }
1199
- async getMarkets() {
1319
+ /** Settled / predicted hourly funding per market_id (WS market_stats snapshot, shared with the public directory). */
1320
+ fundingStats() {
1321
+ return fetchLighterFundingStats(this.venueId, this.networkName);
1322
+ }
1323
+ /** Why the last listing's fundingRate is null (the settled-rate snapshot failed), else null. */
1324
+ fundingWarning() {
1325
+ return lighterFundingStatsWarning(this.venueId, this.networkName);
1326
+ }
1327
+ async getMarkets(opts) {
1200
1328
  if (this._marketsCache && Date.now() - this._marketsCacheTime < LighterAdapter.CACHE_TTL)
1201
1329
  return this._marketsCache;
1330
+ const withFunding = opts?.funding !== false;
1202
1331
  const markets = [];
1203
1332
  // Ensure _marketDecimals is populated (has supported_size_decimals per symbol)
1204
1333
  await this.ensureMarketMap();
1205
1334
  // Fetch order book details and funding rates in parallel
1206
1335
  const [obRes, fundingRates] = await Promise.all([
1207
1336
  this.restGet("/orderBookDetails", {}),
1208
- this.getFundingRates(),
1337
+ // Settled + predicted hourly rates (WS market_stats snapshot — the public directory and
1338
+ // perp-ws read the same source). Unreadable → fundingRate null (unknown), reason printed.
1339
+ !withFunding ? Promise.resolve(new Map()) : this.fundingStats().catch((e) => {
1340
+ console.error(`[${this.name}] settled funding rate unavailable (fundingRate null): ${e instanceof Error ? e.message : String(e)}`);
1341
+ return new Map();
1342
+ }),
1209
1343
  ]);
1210
1344
  // An absent list is a failed fetch, never "no markets" (SSOT Rule #2).
1211
1345
  if (!Array.isArray(obRes.order_book_details)) {
1212
1346
  throw new PerpError("EXCHANGE_ERROR", `${this.name} orderBookDetails: missing order_book_details array`, { exchange: this.name });
1213
1347
  }
1214
1348
  const isPriceStr = (v) => typeof v === "string" && v.length > 0 && Number(v) > 0;
1215
- const perps = obRes.order_book_details.filter(d => d.market_type === "perp");
1349
+ // Live perps only (status "active", not frozen): the one rule shared with the WS directory and
1350
+ // lighter-public.ts. Inactive markets (26 on mainnet, 2026-10-19) were listed as live.
1351
+ const perps = obRes.order_book_details.filter(d => d.market_type === "perp" && isLighterMarketLive(d));
1216
1352
  const unpriced = [];
1217
1353
  for (const d of perps) {
1218
1354
  const imf = d.min_initial_margin_fraction ?? d.default_initial_margin_fraction ?? 500;
1219
1355
  const maxLev = imf > 0 ? Math.floor(10000 / imf) : 50;
1220
- const fr = fundingRates.get(d.symbol);
1356
+ const fr = fundingRates.get(d.market_id);
1221
1357
  const dec = this._marketDecimals.get(d.symbol.toUpperCase());
1222
1358
  // markPrice = the venue's MARK price: orderBookDetails `mark_price` (the source the public
1223
1359
  // data path lighter-public.ts uses; /funding-rates carries no mark price). last_trade_price
@@ -1230,13 +1366,20 @@ export class LighterAdapter {
1230
1366
  symbol: d.symbol,
1231
1367
  markPrice: d.mark_price,
1232
1368
  indexPrice: isPriceStr(d.index_price) ? d.index_price : null,
1369
+ // Last settled hourly rate (market_stats funding_rate, = the newest /fundings print, = the
1370
+ // perp-ws `rate`), a fraction per 1 h; the running hour's predicted rate is additive.
1233
1371
  fundingRate: fr?.rate ?? null,
1372
+ fundingRateKind: fr?.rate != null ? "lastSettled" : null,
1373
+ predictedFundingRate: fr?.predictedRate ?? null,
1234
1374
  volume24h: d.daily_quote_token_volume != null ? String(d.daily_quote_token_volume) : null,
1375
+ // BASE units, one side (WS market_stats open_interest is the same quantity in USD).
1235
1376
  openInterest: d.open_interest != null ? String(d.open_interest) : null,
1236
1377
  maxLeverage: maxLev,
1237
1378
  sizeDecimals: dec ? dec.size : null,
1238
- stepSize: null,
1239
- fundingHours: null,
1379
+ // The lot grid of the size decimals (the public directory's stepSize, lighter-public.ts).
1380
+ stepSize: dec ? (dec.size === 0 ? "1" : `0.${"0".repeat(dec.size - 1)}1`) : null,
1381
+ // `fundingRate` is per 1 h (hourly settlement).
1382
+ fundingHours: 1,
1240
1383
  marginTiers: null,
1241
1384
  });
1242
1385
  }
@@ -1248,8 +1391,11 @@ export class LighterAdapter {
1248
1391
  exchange: this.name, remediation: "Retry later; the venue market data is incomplete",
1249
1392
  });
1250
1393
  }
1251
- this._marketsCache = markets;
1252
- this._marketsCacheTime = Date.now();
1394
+ // A marks-only listing (no funding read) is not cached as the full one.
1395
+ if (withFunding) {
1396
+ this._marketsCache = markets;
1397
+ this._marketsCacheTime = Date.now();
1398
+ }
1253
1399
  return markets;
1254
1400
  }
1255
1401
  get accountCacheKey() {
@@ -1283,27 +1429,22 @@ export class LighterAdapter {
1283
1429
  * Book from GET /orderBookOrders?market_id=<id> on this venue's host (Lighter RH: its own host
1284
1430
  * and market ids, e.g. RH testnet TSLA = 112). Rule #2: an unknown symbol (SYMBOL_NOT_FOUND) or
1285
1431
  * a failed read throws — it is never reported as an empty book. An empty `bids`/`asks` is the
1286
- * venue's real answer (no liquidity).
1432
+ * venue's real answer (no liquidity). The endpoint lists individual ORDERS (up to 250 per side,
1433
+ * the documented max); they are aggregated into price levels (lighterBookLevels), as the WS
1434
+ * `order_book` channel publishes them.
1287
1435
  */
1288
- async getOrderbook(symbol) {
1436
+ async getOrderbook(symbol, depth) {
1289
1437
  await this.ensureMarketMap();
1290
1438
  const marketId = this.getMarketIndex(symbol);
1291
1439
  const res = await this.restGet("/orderBookOrders", {
1292
1440
  market_id: String(marketId),
1293
- limit: "50",
1441
+ limit: String(LIGHTER_BOOK_ORDERS_LIMIT),
1294
1442
  });
1295
- const level = (l) => {
1296
- const size = l.remaining_base_amount ?? l.size;
1297
- if (typeof l.price !== "string" || typeof size !== "string") {
1298
- throw new PerpError("EXCHANGE_ERROR", `${this.name} orderBookOrders ${symbol}: level without price / size`, { exchange: this.name });
1299
- }
1300
- return [l.price, size];
1301
- };
1302
- return { bids: (res.bids ?? []).map(level), asks: (res.asks ?? []).map(level) };
1443
+ return lighterBook(this.name, res, symbol, depth);
1303
1444
  }
1304
1445
  async getBalance() {
1305
1446
  if (!this._address)
1306
- throw new Error("No private key configured — account data unavailable. Run: perp setup");
1447
+ throw noAccountKeyError(this.name);
1307
1448
  const acct = await this.fetchAccount();
1308
1449
  // Rule #2: no account is not a zero balance — say so (like getMarketSettings).
1309
1450
  if (!acct) {
@@ -1312,58 +1453,15 @@ export class LighterAdapter {
1312
1453
  remediation: `A ${this.name} account is created by the first deposit; check the address / account index (perp -e ${this.name} wallet show) or deposit first`,
1313
1454
  });
1314
1455
  }
1315
- const totalAsset = parseFiniteVenueNumber(acct.total_asset_value, "total_asset_value", "lighter");
1316
- const available = parseFiniteVenueNumber(acct.available_balance, "available_balance", "lighter");
1317
- const collateral = parseFiniteVenueNumber(acct.collateral, "collateral", "lighter");
1318
- const unrealizedPnl = acct.positions?.reduce((sum, p) => sum + parseFiniteVenueNumber(p.unrealized_pnl, "position.unrealized_pnl", "lighter"), 0) ?? 0;
1319
- // Include spot collateral balance (USDC on Lighter, USDG on Lighter RH; separate from perp collateral)
1320
- // An account without that asset holds none of it (0); an unreadable assets list / balance throws.
1321
- let spotUsdc = 0;
1322
- const collateralSymbol = this.collateralSymbol;
1323
- if (!Array.isArray(acct.assets)) {
1324
- throw new PerpError("EXCHANGE_ERROR", `${this.name} account has no assets list — spot ${collateralSymbol} balance unknown`, { exchange: this.name });
1325
- }
1326
- const usdcAsset = acct.assets.find(a => a?.symbol === collateralSymbol);
1327
- if (usdcAsset !== undefined)
1328
- spotUsdc = parseFiniteVenueNumber(usdcAsset.balance, `assets.${collateralSymbol}.balance`, "lighter");
1329
- return {
1330
- equity: String(totalAsset + spotUsdc),
1331
- available: String(available),
1332
- marginUsed: String(Math.max(0, collateral - available)),
1333
- unrealizedPnl: String(unrealizedPnl),
1334
- };
1456
+ return lighterBalanceOf(acct, this.name, this.collateralSymbol);
1335
1457
  }
1336
1458
  async getPositions() {
1337
1459
  if (!this._address)
1338
- throw new Error("No private key configured — account data unavailable. Run: perp setup");
1460
+ throw noAccountKeyError(this.name);
1339
1461
  const acct = await this.fetchAccount();
1340
1462
  if (!acct)
1341
1463
  return [];
1342
- return (acct.positions ?? [])
1343
- .filter((p) => parseFiniteVenueNumber(p.position, "position", "lighter") !== 0)
1344
- .map((p) => {
1345
- const posSize = parseFiniteVenueNumber(p.position, "position", "lighter");
1346
- const positionValue = parseFiniteVenueNumber(p.position_value, "position_value", "lighter");
1347
- return {
1348
- symbol: String(p.symbol || `Market-${p.market_id}`),
1349
- side: (Number(p.sign) > 0 ? "long" : "short"),
1350
- size: String(Math.abs(posSize)),
1351
- entryPrice: String(p.avg_entry_price || "0"),
1352
- markPrice: (() => {
1353
- if (posSize === 0)
1354
- return "0";
1355
- const rawMark = positionValue / Math.abs(posSize);
1356
- const priceDec = this._marketDecimals.get(String(p.symbol || "").toUpperCase())?.price;
1357
- return priceDec !== undefined ? rawMark.toFixed(priceDec) : formatComputedPrice(rawMark);
1358
- })(),
1359
- // The venue reports "0" when the position has no liquidation price (e.g. cross margin
1360
- // well covered): that is "none", not a price.
1361
- liquidationPrice: Number(p.liquidation_price) > 0 ? String(p.liquidation_price) : null,
1362
- unrealizedPnl: String(p.unrealized_pnl || "0"),
1363
- // The leverage CONFIGURED for the market (what `trade leverage` set) — not notional / equity.
1364
- ...lighterPositionSettings(p, this.name),
1365
- };
1366
- });
1464
+ return lighterPositionRows(acct.positions, this.name, (sym) => this._marketDecimals.get(sym.toUpperCase())?.price);
1367
1465
  }
1368
1466
  /**
1369
1467
  * Per-market leverage / margin mode the venue exposes: every `positions[]` entry of the
@@ -1372,7 +1470,7 @@ export class LighterAdapter {
1372
1470
  */
1373
1471
  async getMarketSettings() {
1374
1472
  if (!this._address)
1375
- throw new Error("No private key configured — account data unavailable. Run: perp setup");
1473
+ throw noAccountKeyError(this.name);
1376
1474
  const acct = await this.fetchAccount();
1377
1475
  if (acct === null)
1378
1476
  throw new PerpError("EXCHANGE_ERROR", `${this.name}: no account found for ${this._address}`, { exchange: this.name });
@@ -1394,7 +1492,7 @@ export class LighterAdapter {
1394
1492
  }
1395
1493
  async getOpenOrders() {
1396
1494
  if (!this._address)
1397
- throw new Error("No private key configured — account data unavailable. Run: perp setup");
1495
+ throw noAccountKeyError(this.name);
1398
1496
  if (this._accountIndex < 0 || this._readOnly)
1399
1497
  return [];
1400
1498
  // ONE request for every perp market. Docs (apidocs.lighter.xyz/reference/accountactiveorders):
@@ -1451,7 +1549,7 @@ export class LighterAdapter {
1451
1549
  return isAsk ? "sell" : "buy";
1452
1550
  }
1453
1551
  /**
1454
- * Slippage-bounded IOC (§13.2): protection price = orderbook mid ± opts.slippage.bps,
1552
+ * Slippage-bounded IOC: protection price = orderbook mid ± opts.slippage.bps,
1455
1553
  * tick-aligned toward crossing. The returned raw sendTx result carries `protection`
1456
1554
  * and, when the venue reported the fill, `iocOutcome` (see iocOutcomeOf).
1457
1555
  * `opts.clientOrderIndex` (optional) = caller's numeric client order id (lighterClientOrderIndex rule).
@@ -1466,8 +1564,9 @@ export class LighterAdapter {
1466
1564
  try {
1467
1565
  this.ensureSigner();
1468
1566
  await this.ensureMarketMap();
1469
- const nonce = await this.getNextNonce();
1470
1567
  const marketIndex = this.getMarketIndex(symbol);
1568
+ this.assertOpenable(symbol, opts.reduceOnly);
1569
+ const nonce = await this.getNextNonce();
1471
1570
  ({ baseAmount } = this.toTicks(symbol, size, "0"));
1472
1571
  const reference = await this.getReferencePrice(symbol);
1473
1572
  const p = this.protectionFor(symbol, side, reference, opts.slippage, LIGHTER_REFERENCE_PRICE_SOURCE);
@@ -1502,9 +1601,9 @@ export class LighterAdapter {
1502
1601
  return this.awaitIocOutcome(result, symbol, baseAmount, `market ${side} ${size} ${symbol}`, { protection, clientOrderIndex }, match);
1503
1602
  }
1504
1603
  /**
1505
- * TIF mapping (§13.3): GTC/GTT → GTT (28-day expiry, gttExpiryMs), IOC → IOC,
1604
+ * TIF mapping: GTC/GTT → GTT (28-day expiry, gttExpiryMs), IOC → IOC,
1506
1605
  * ALO / POST_ONLY → time-in-force 2 (POST_ONLY per apidocs trading.md and the official
1507
- * lighter-go signer; 28-day expiry like GTT — testnet confirmation pending, design §11).
1606
+ * lighter-go signer; 28-day expiry like GTT — testnet confirmation pending).
1508
1607
  * FOK and any other value → CAPABILITY_UNSUPPORTED (never silently GTT).
1509
1608
  * `opts.clientOrderIndex` as in marketOrder.
1510
1609
  * IOC: the same definite outcome as marketOrder (awaitIocOutcome — zero fill →
@@ -1530,8 +1629,9 @@ export class LighterAdapter {
1530
1629
  throw unsupported(this.name, tif === "FOK" ? "fok" : "timeInForce", `time-in-force ${opts?.tif} is not offered`, "Use --tif GTC or IOC");
1531
1630
  this.ensureSigner();
1532
1631
  await this.ensureMarketMap();
1533
- const nonce = await this.getNextNonce();
1534
1632
  const marketIndex = this.getMarketIndex(symbol);
1633
+ this.assertOpenable(symbol, opts?.reduceOnly === true);
1634
+ const nonce = await this.getNextNonce();
1535
1635
  const ticks = this.toTicks(symbol, size, price);
1536
1636
  sentBase = ticks.baseAmount;
1537
1637
  isIoc = timeInForce === LIGHTER_TIF.IOC;
@@ -1695,6 +1795,7 @@ export class LighterAdapter {
1695
1795
  // The order must be an open order of this market, and not a trigger order: a modify is
1696
1796
  // signed with triggerPrice 0, which would turn a stop / take-profit into a plain limit.
1697
1797
  existing = await this.activeOrderRow(symbol, marketIndex, orderId, "modified");
1798
+ this.assertOpenable(symbol, existing.reduce_only === true || existing.reduce_only === 1);
1698
1799
  const type = String(existing.type ?? "").toLowerCase();
1699
1800
  if (/stop|take|trigger/.test(type) || Number(existing.trigger_price ?? 0) > 0) {
1700
1801
  throw unsupported(this.name, "amend", "trigger orders cannot be modified", "cancel and re-place");
@@ -1841,7 +1942,7 @@ export class LighterAdapter {
1841
1942
  /**
1842
1943
  * Trigger order. Market trigger (no limitPrice): execution price bound = trigger price ±
1843
1944
  * `opts.slippage` bps (the caller's resolveSlippage() output — required; the adapter never
1844
- * reads settings, §13.10 #1), tick-aligned toward crossing (§13.10 #3), recorded in the
1945
+ * reads settings), tick-aligned toward crossing, recorded in the
1845
1946
  * result as `protection`. Accepted risk: a fast move through the bound leaves the
1846
1947
  * triggered order unfilled.
1847
1948
  * TP vs SL: `opts.kind` given (TP/SL legs) → that type as given (a trigger already crossed
@@ -1864,8 +1965,9 @@ export class LighterAdapter {
1864
1965
  }
1865
1966
  this.ensureSigner();
1866
1967
  await this.ensureMarketMap();
1867
- const nonce = await this.getNextNonce();
1868
1968
  const marketIndex = this.getMarketIndex(symbol);
1969
+ this.assertOpenable(symbol, opts?.reduceOnly === true);
1970
+ const nonce = await this.getNextNonce();
1869
1971
  const { baseAmount } = this.toTicks(symbol, size, "0");
1870
1972
  const { priceTicks: triggerTicks } = this.toTicks(symbol, "0", triggerPrice);
1871
1973
  const triggerNum = parseFloat(triggerPrice);
@@ -2000,6 +2102,12 @@ export class LighterAdapter {
2000
2102
  return this.updateLeverage(symbol, leverage, marginMode);
2001
2103
  }
2002
2104
  async getRecentTrades(symbol, limit = 20) {
2105
+ // /recentTrades `limit`: 1..100 (apidocs.lighter.xyz/reference/recenttrades; 101 → code 20001).
2106
+ if (!Number.isInteger(limit) || limit < 1 || limit > LIGHTER_RECENT_TRADES_MAX) {
2107
+ throw new PerpError("INVALID_PARAMS", `${this.name} trades limit must be 1..${LIGHTER_RECENT_TRADES_MAX} (got ${limit})`, {
2108
+ exchange: this.name, remediation: `${this.name} serves at most the last ${LIGHTER_RECENT_TRADES_MAX} trades`,
2109
+ });
2110
+ }
2003
2111
  await this.ensureMarketMap();
2004
2112
  const marketId = this.getMarketIndex(symbol);
2005
2113
  const res = await this.restGet("/recentTrades", { market_id: String(marketId), limit: String(limit) });
@@ -2013,30 +2121,128 @@ export class LighterAdapter {
2013
2121
  side: t.is_maker_ask ? "buy" : "sell",
2014
2122
  price: String(t.price ?? "0"),
2015
2123
  size: String(t.size ?? t.base_amount ?? t.amount ?? ""),
2016
- fee: String(t.fee ?? "0"),
2124
+ // The tape's taker_fee / maker_fee are the parties' fee RATES, not a fee the viewer paid.
2125
+ fee: null,
2017
2126
  }));
2018
2127
  }
2128
+ /**
2129
+ * Settled funding prints: GET /api/v1/fundings (apidocs.lighter.xyz/reference/fundings;
2130
+ * `count_back` rows from `start_timestamp`, max 750). Each row is one hourly settlement:
2131
+ * `rate` is PERCENT per 1 h, unsigned, with `direction` "long" (longs pay → positive) or
2132
+ * "short" (shorts pay → negative). Live 2026-10-19 BTC: rate "0.0012", value "0.99315" =
2133
+ * mark 82,763 × 0.000012, i.e. value / mark = rate / 100. NOT the /funding-rates unit (8 h).
2134
+ */
2019
2135
  async getFundingHistory(symbol, limit = 10) {
2020
- const rates = await this.getFundingRates();
2021
- const r = rates.get(symbol.toUpperCase());
2022
- if (!r)
2023
- return [];
2024
- // Lighter funding-rates is current only, not historical
2025
- return [{ time: Date.now(), rate: r.rate, price: r.markPrice }];
2136
+ if (!Number.isInteger(limit) || limit < 1 || limit > 750) {
2137
+ throw new PerpError("INVALID_PARAMS", `${this.name} funding history limit must be 1..750 (got ${limit})`, { exchange: this.name });
2138
+ }
2139
+ await this.ensureMarketMap();
2140
+ const marketId = this.getMarketIndex(symbol); // unlisted market: SYMBOL_NOT_FOUND, not an empty history
2141
+ const end = Math.floor(Date.now() / 1000);
2142
+ const res = await this.restGet("/fundings", {
2143
+ market_id: String(marketId),
2144
+ resolution: "1h",
2145
+ // One spare hour covers the settlement in progress.
2146
+ start_timestamp: String(end - (limit + 1) * 3600),
2147
+ end_timestamp: String(end),
2148
+ count_back: String(limit),
2149
+ });
2150
+ if (!Array.isArray(res.fundings)) {
2151
+ throw new PerpError("EXCHANGE_ERROR", `${this.name} /fundings: missing fundings array (${JSON.stringify(res).slice(0, 120)})`, { exchange: this.name });
2152
+ }
2153
+ return res.fundings.map((f) => lighterFundingRow(this.name, f)).sort((a, b) => a.time - b.time).slice(-limit);
2026
2154
  }
2155
+ /**
2156
+ * Candles opening in [startTime, endTime] (ms). GET /api/v1/candles answers the
2157
+ * max(count_back, bars in the window) newest bars ending at `end_timestamp`, at most 500
2158
+ * (live 2026-10-19, BTC 1h: a 96-bar window with count_back 1 / 96 → those 96 bars; with
2159
+ * count_back 500 → 500 bars, 404 of them before start_timestamp; a future window → past bars).
2160
+ * So count_back is the window's bar count (≤ 500), rows outside the window are dropped, and a
2161
+ * longer window is paged backwards in 500-bar chunks by moving end_timestamp. `end_timestamp` is
2162
+ * exclusive at second resolution (live: end = a bar's open excludes it, open + 1 s includes it),
2163
+ * so end + 1 s is sent for an inclusive --end. Each answer's `r`
2164
+ * must be the requested resolution. Intervals outside LIGHTER_CANDLE_INTERVALS and windows over
2165
+ * LIGHTER_KLINE_MAX_PAGES pages are refused with INVALID_PARAMS before any request.
2166
+ */
2027
2167
  async getKlines(symbol, interval, startTime, endTime) {
2028
- const res = await this.getCandles(symbol, interval, startTime, endTime);
2029
- // /candles returns the array under key `c` (top keys: code, r, c) — `res.candles`
2030
- // is always undefined, which silently rendered every Lighter kline set empty.
2031
- const candles = (res.c ?? res.candles ?? []);
2168
+ const ivMs = LIGHTER_CANDLE_INTERVALS[interval];
2169
+ if (ivMs === undefined) {
2170
+ throw new PerpError("INVALID_PARAMS", `${this.name} kline interval ${JSON.stringify(interval)} is not served (supported: ${Object.keys(LIGHTER_CANDLE_INTERVALS).join(", ")})`, {
2171
+ exchange: this.name, remediation: `Use one of: ${Object.keys(LIGHTER_CANDLE_INTERVALS).join(", ")}`,
2172
+ });
2173
+ }
2174
+ if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || startTime > endTime) {
2175
+ throw new PerpError("INVALID_PARAMS", `${this.name} kline window must be start ≤ end (unix ms; got ${startTime}..${endTime})`, { exchange: this.name });
2176
+ }
2177
+ // Bar opens in [start, end] (bars open on multiples of the interval since the epoch).
2178
+ const barsIn = (from, to) => Math.floor(to / ivMs) - Math.ceil(from / ivMs) + 1;
2179
+ const bars = barsIn(startTime, endTime);
2180
+ if (bars > LIGHTER_CANDLES_PER_CALL * LIGHTER_KLINE_MAX_PAGES) {
2181
+ throw new PerpError("INVALID_PARAMS", `${this.name} kline window spans ${bars} ${interval} bars; at most ${LIGHTER_CANDLES_PER_CALL * LIGHTER_KLINE_MAX_PAGES} are read (${LIGHTER_KLINE_MAX_PAGES} requests of ${LIGHTER_CANDLES_PER_CALL})`, {
2182
+ exchange: this.name, remediation: "Narrow --start / --end or use a longer interval",
2183
+ });
2184
+ }
2185
+ if (bars <= 0) {
2186
+ // No bar opens in the window: nothing to ask for (count_back 0 is never sent); the symbol is still checked.
2187
+ await this.ensureMarketMap();
2188
+ this.getMarketIndex(symbol);
2189
+ return [];
2190
+ }
2191
+ const byTime = new Map();
2192
+ let cursorEnd = endTime;
2193
+ let prevEarliest = Infinity;
2194
+ let covered = false;
2195
+ for (let page = 0; page < LIGHTER_KLINE_MAX_PAGES; page++) {
2196
+ const left = barsIn(startTime, cursorEnd);
2197
+ if (left <= 0) {
2198
+ covered = true;
2199
+ break;
2200
+ } // every bar open in the window is read
2201
+ const want = Math.min(LIGHTER_CANDLES_PER_CALL, left);
2202
+ const res = await this.getCandles(symbol, interval, startTime, cursorEnd + 1000, want);
2203
+ assertCandleInterval(this.name, interval, res.r);
2204
+ // /candles returns the array under key `c` (top keys: code, r, c).
2205
+ if (!Array.isArray(res.c)) {
2206
+ throw new PerpError("EXCHANGE_ERROR", `${this.name} /candles: no candle list (${JSON.stringify(res).slice(0, 120)})`, { exchange: this.name });
2207
+ }
2208
+ const rows = res.c;
2209
+ let earliest = Infinity;
2210
+ for (const c of rows) {
2211
+ const t = Number(c.t);
2212
+ if (!Number.isFinite(t))
2213
+ throw new PerpError("EXCHANGE_ERROR", `${this.name} /candles: row without an open time: ${JSON.stringify(c).slice(0, 120)}`, { exchange: this.name });
2214
+ if (t < earliest)
2215
+ earliest = t;
2216
+ if (t >= startTime && t <= endTime)
2217
+ byTime.set(t, c);
2218
+ }
2219
+ // Done: the window start is covered, or the venue has no older bars (a short answer).
2220
+ if (rows.length === 0 || earliest <= startTime || rows.length < want) {
2221
+ covered = true;
2222
+ break;
2223
+ }
2224
+ if (earliest >= prevEarliest) {
2225
+ throw new PerpError("EXCHANGE_ERROR", `${this.name} /candles: paging made no progress (oldest bar ${earliest} not before the previous page's ${prevEarliest})`, { exchange: this.name });
2226
+ }
2227
+ prevEarliest = earliest;
2228
+ cursorEnd = earliest - 1000; // sent as `earliest`: the bars strictly before the oldest one read
2229
+ }
2230
+ if (!covered) {
2231
+ // Every page was full and the window start is still not reached: never a silently short window.
2232
+ throw new PerpError("EXCHANGE_ERROR", `${this.name} /candles: ${LIGHTER_KLINE_MAX_PAGES} full pages did not reach the window start ${startTime} (oldest ${prevEarliest})`, { exchange: this.name });
2233
+ }
2234
+ const candles = [...byTime.values()].sort((a, b) => Number(a.t) - Number(b.t));
2032
2235
  return candles.map((c) => ({
2033
- time: Number(c.start_timestamp ?? c.t ?? 0),
2236
+ time: Number(c.t),
2034
2237
  open: String(c.open ?? c.o ?? "0"),
2035
2238
  high: String(c.high ?? c.h ?? "0"),
2036
2239
  low: String(c.low ?? c.l ?? "0"),
2037
2240
  close: String(c.close ?? c.c ?? "0"),
2038
- volume: String(c.base_token_volume ?? c.v ?? ""),
2039
- trades: Number(c.trades_count ?? c.n ?? 0),
2241
+ volume: candleNumber(c.base_token_volume ?? c.v),
2242
+ // /candles: v = base volume, V = quote (USD) volume; no per-candle trade count (live
2243
+ // 2026-10-14 keys: t o h l c v V i — `i` is the last trade id, not a count).
2244
+ quoteVolume: candleNumber(c.V),
2245
+ trades: typeof c.trades_count === "number" ? c.trades_count : typeof c.n === "number" ? c.n : null,
2040
2246
  }));
2041
2247
  }
2042
2248
  /**
@@ -2095,7 +2301,7 @@ export class LighterAdapter {
2095
2301
  */
2096
2302
  async getOrderHistory(limit = 30) {
2097
2303
  if (!this._address)
2098
- throw new Error("No private key configured — account data unavailable. Run: perp setup");
2304
+ throw noAccountKeyError(this.name);
2099
2305
  const orders = await this.historyRows("/accountInactiveOrders", "orders", { market_type: "perp" }, limit, true);
2100
2306
  // Every field is required: a missing price / size / status / type is an unreadable row, never "0" / "done" / "limit".
2101
2307
  const req = (o, k) => lighterRowField(o, k, this.name, "order history");
@@ -2125,7 +2331,7 @@ export class LighterAdapter {
2125
2331
  */
2126
2332
  async getTradeHistory(limit = 30) {
2127
2333
  if (!this._address)
2128
- throw new Error("No private key configured — account data unavailable. Run: perp setup");
2334
+ throw noAccountKeyError(this.name);
2129
2335
  const trades = await this.historyRows("/trades", "trades", { sort_by: "timestamp", market_type: "perp" }, limit, false);
2130
2336
  return trades.map((t) => {
2131
2337
  const sold = t.ask_account_id === this._accountIndex;
@@ -2160,7 +2366,7 @@ export class LighterAdapter {
2160
2366
  */
2161
2367
  async getFundingPayments(limit = 200) {
2162
2368
  if (!this._address)
2163
- throw new Error("No private key configured — account data unavailable. Run: perp setup");
2369
+ throw noAccountKeyError(this.name);
2164
2370
  const items = await this.historyRows("/positionFunding", "position_fundings", { side: "all" }, limit, true);
2165
2371
  return items.map((f) => {
2166
2372
  if (typeof f.timestamp !== "number" || typeof f.change !== "string") {
@@ -2170,8 +2376,9 @@ export class LighterAdapter {
2170
2376
  });
2171
2377
  }
2172
2378
  /**
2173
- * Get funding rates for all markets.
2174
- * API: GET /api/v1/funding-rates
2379
+ * GET /api/v1/funding-rates: the venue's PREDICTED rate × 8, a fraction per 8 h — not the venue
2380
+ * basis (1 h; market rows carry the settled hourly rate, fundingStats). Not used for funding
2381
+ * comparisons; normalize with 8 h if read.
2175
2382
  */
2176
2383
  async getFundingRates() {
2177
2384
  const map = new Map();
@@ -2428,7 +2635,7 @@ export class LighterAdapter {
2428
2635
  }
2429
2636
  }
2430
2637
  /**
2431
- * One WASM signer (one Go runtime) per `${venueId}:${network}` (design §10.5).
2638
+ * One WASM signer (one Go runtime) per `${venueId}:${network}`.
2432
2639
  * lighter-go keys its clients by (accountIndex, apiKeyIndex) only and keeps a
2433
2640
  * package-global chainId for L1 messages, so two venues must never share a
2434
2641
  * runtime. Keyed by venue+network rather than chainId because Lighter testnet