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,99 +1,13 @@
1
1
  import { makeTable, formatUsd, formatPercent, formatPnl, printJson, jsonOk, withJsonErrors, logSettledRejections } from "../utils.js";
2
2
  import chalk from "chalk";
3
3
  import { toHourlyRate, annualizeHourlyRate, estimateHourlyFundingFromHourly } from "../funding.js";
4
- import { fetchAllFundingRates, fetchSymbolFundingRates, TOP_SYMBOLS, } from "../funding-rates.js";
4
+ import { fetchAllFundingRates, fetchCanonicalPriceGroups, dropPriceOutliers, fetchSymbolFundingRates, isRankedComparison, TOP_SYMBOLS, } from "../funding-rates.js";
5
5
  import { saveFundingSnapshot, getHistoricalRates, getCompoundedAnnualReturn, getExchangeCompoundingHours, } from "../funding-history.js";
6
6
  import { getVenue, listVenuesWithRole, tryResolveVenueId } from "../exchanges/venue.js";
7
7
  import "../exchanges/venues/index.js";
8
- import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates, fetchPacificaPricesRaw, parsePacificaRaw, fetchHyperliquidAllMidsRaw, fetchLighterOrderBookDetailsRaw, } from "../shared-api.js";
8
+ import { MAX_PRICE_DEVIATION_PCT } from "../exchanges/index.js";
9
+ import { fetchPacificaPricesRaw, parsePacificaRaw, fetchHyperliquidAllMidsRaw, fetchLighterOrderBookDetailsRaw, } from "../shared-api.js";
9
10
  import { scanDexArb } from "../dex-asset-map.js";
10
- async function fetchPacificaRates() {
11
- const assets = await fetchPacificaPrices();
12
- return assets.map(p => ({
13
- exchange: "pacifica",
14
- symbol: p.symbol,
15
- fundingRate: p.funding,
16
- markPrice: p.mark,
17
- }));
18
- }
19
- async function fetchHyperliquidRates() {
20
- const assets = await fetchHyperliquidMeta();
21
- return assets.map(a => ({
22
- exchange: "hyperliquid",
23
- symbol: a.symbol,
24
- fundingRate: a.funding,
25
- markPrice: a.markPx,
26
- }));
27
- }
28
- async function fetchAsterRates() {
29
- // SSOT rule #2: do NOT silently coerce missing markPrice / lastFundingRate
30
- // to 0 — that would corrupt comparisons against other DEXs. Skip symbols
31
- // whose Aster row is malformed instead. An endpoint failure PROPAGATES: the
32
- // caller (handleBasisScan) runs every DEX under Promise.allSettled and reports
33
- // the rejection on stderr — Aster is never silently absent from the scan.
34
- const res = await fetch("https://fapi.asterdex.com/fapi/v1/premiumIndex");
35
- if (!res.ok)
36
- throw new Error(`Aster premiumIndex HTTP ${res.status}`);
37
- const premiums = await res.json();
38
- if (!Array.isArray(premiums))
39
- throw new Error("Aster premiumIndex returned a non-array response");
40
- const out = [];
41
- for (const p of premiums) {
42
- const rawSym = String(p.symbol ?? "");
43
- if (!rawSym.endsWith("USDT"))
44
- continue;
45
- const fundingRate = Number(p.lastFundingRate);
46
- const markPrice = Number(p.markPrice);
47
- if (!Number.isFinite(fundingRate) || !Number.isFinite(markPrice) || markPrice <= 0) {
48
- continue;
49
- }
50
- out.push({
51
- exchange: "aster",
52
- symbol: rawSym.replace(/USDT$/, ""),
53
- fundingRate,
54
- markPrice,
55
- });
56
- }
57
- return out;
58
- }
59
- async function fetchLighterRates() {
60
- // SSOT rule #2: error must propagate. Caller (handleBasisScan) uses
61
- // Promise.allSettled to keep multi-DEX comparison alive when one DEX is
62
- // down, with explicit stderr logging.
63
- const [details, funding] = await Promise.all([
64
- fetchLighterOrderBookDetails(),
65
- fetchLighterFundingRates(),
66
- ]);
67
- const priceMap = new Map(details.map(d => [d.marketId, d.lastTradePrice]));
68
- const symMap = new Map(details.map(d => [d.marketId, d.symbol]));
69
- const rates = [];
70
- for (const fr of funding) {
71
- const symbol = fr.symbol || symMap.get(fr.marketId);
72
- if (!symbol)
73
- continue;
74
- // Prefer fr.markPrice; fall back to orderBook last-trade as documented
75
- // price-source preference (NOT error fallback). Skip if neither resolves
76
- // — never publish a 0 markPrice downstream.
77
- const directMark = fr.markPrice;
78
- const fallbackMark = priceMap.get(fr.marketId);
79
- let markPrice;
80
- if (directMark !== null && Number.isFinite(directMark) && directMark > 0) {
81
- markPrice = directMark;
82
- }
83
- else if (fallbackMark !== undefined && Number.isFinite(fallbackMark) && fallbackMark > 0) {
84
- markPrice = fallbackMark;
85
- }
86
- if (markPrice === undefined || !Number.isFinite(fr.rate))
87
- continue;
88
- rates.push({
89
- exchange: "lighter",
90
- symbol,
91
- fundingRate: fr.rate,
92
- markPrice,
93
- });
94
- }
95
- return rates;
96
- }
97
11
  // annualize moved to ../funding.ts — use annualizeRate() / computeAnnualSpread()
98
12
  export function registerArbCommands(program, isJson, getAdapterForExchange) {
99
13
  const arb = program.command("arb").description("Funding rate arbitrage & basis trading");
@@ -132,23 +46,34 @@ export async function fetchAllPrices() {
132
46
  ltPrices.set(sym, price);
133
47
  }
134
48
  }
49
+ return buildGapSnapshots(pacPrices, hlPrices, ltPrices);
50
+ }
51
+ /**
52
+ * Cross-venue price gaps by venue symbol. The same ticker on two venues is not proof of the same
53
+ * instrument (Pacifica SAMSUNG 165.8 USD vs Lighter SAMSUNG 220,597 — a KRW-quoted listing): each
54
+ * symbol's prices go through the funding scan's median price-outlier guard (dropPriceOutliers,
55
+ * MAX_PRICE_DEVIATION_PCT, one warning per symbol / venue) before a gap is computed, so a gap is
56
+ * only reported between prices that agree within the guard.
57
+ */
58
+ export function buildGapSnapshots(pacPrices, hlPrices, ltPrices) {
135
59
  const allSymbols = new Set([...pacPrices.keys(), ...hlPrices.keys(), ...ltPrices.keys()]);
136
60
  const snapshots = [];
61
+ const LABEL = { pacifica: "PAC", hyperliquid: "HL", lighter: "LT" };
137
62
  for (const sym of allSymbols) {
138
- const pac = pacPrices.get(sym) ?? null;
139
- const hl = hlPrices.get(sym) ?? null;
140
- const lt = ltPrices.get(sym) ?? null;
141
- // Need at least 2 exchanges
142
- const available = [];
143
- if (pac !== null)
144
- available.push({ ex: "PAC", price: pac });
145
- if (hl !== null)
146
- available.push({ ex: "HL", price: hl });
147
- if (lt !== null)
148
- available.push({ ex: "LT", price: lt });
149
- if (available.length < 2)
63
+ const listed = [];
64
+ for (const [exchange, map] of [["pacifica", pacPrices], ["hyperliquid", hlPrices], ["lighter", ltPrices]]) {
65
+ const p = map.get(sym);
66
+ if (p !== undefined)
67
+ listed.push({ exchange, markPrice: p });
68
+ }
69
+ if (listed.length < 2)
70
+ continue;
71
+ const kept = dropPriceOutliers(sym, listed, "gaps");
72
+ // Need at least 2 exchanges whose prices agree
73
+ if (kept.length < 2)
150
74
  continue;
151
- available.sort((a, b) => a.price - b.price);
75
+ const priceOf = (ex) => kept.find(k => k.exchange === ex)?.markPrice ?? null;
76
+ const available = kept.map(k => ({ ex: LABEL[k.exchange], price: k.markPrice })).sort((a, b) => a.price - b.price);
152
77
  const cheapest = available[0];
153
78
  const expensive = available[available.length - 1];
154
79
  const maxGap = expensive.price - cheapest.price;
@@ -156,9 +81,9 @@ export async function fetchAllPrices() {
156
81
  const maxGapPct = mid > 0 ? (maxGap / mid) * 100 : 0;
157
82
  snapshots.push({
158
83
  symbol: sym,
159
- pacPrice: pac,
160
- hlPrice: hl,
161
- ltPrice: lt,
84
+ pacPrice: priceOf("pacifica"),
85
+ hlPrice: priceOf("hyperliquid"),
86
+ ltPrice: priceOf("lighter"),
162
87
  maxGap,
163
88
  maxGapPct,
164
89
  cheapest: cheapest.ex,
@@ -280,7 +205,8 @@ function printSnapshotTable(snapshot) {
280
205
  }
281
206
  const rows = snapshot.symbols.map(s => {
282
207
  const rateOf = (id) => s.rates.find(r => r.exchange === id);
283
- const spreadColor = s.maxSpreadAnnual >= 30 ? chalk.green : s.maxSpreadAnnual >= 10 ? chalk.yellow : chalk.white;
208
+ const spread = s.maxSpreadAnnual;
209
+ const spreadColor = spread === null ? chalk.gray : spread >= 30 ? chalk.green : spread >= 10 ? chalk.yellow : chalk.white;
284
210
  // Historical-average source preference: HL, PAC, LT, AST, then whichever venue quoted.
285
211
  const bestRate = rateOf("hyperliquid") ?? rateOf("pacifica") ?? rateOf("lighter") ?? rateOf("aster") ?? s.rates[0];
286
212
  const trend = trendMap.get(s.symbol) ?? chalk.gray("-");
@@ -290,13 +216,18 @@ function printSnapshotTable(snapshot) {
290
216
  const r = rateOf(v.id);
291
217
  return r ? formatPercent(r.fundingRate) : chalk.gray("-");
292
218
  }),
293
- spreadColor(`${s.maxSpreadAnnual.toFixed(1)}%`), trend,
294
- s.maxSpreadAnnual >= 5 ? `${exAbbr(s.shortExchange)}>${exAbbr(s.longExchange)}` : chalk.gray("-"),
219
+ // No ranked pair (< 2 verified rows): unverified rates are shown, never ranked.
220
+ spread === null ? chalk.gray("n/a") : spreadColor(`${spread.toFixed(1)}%`), trend,
221
+ spread === null || s.longExchange === null || s.shortExchange === null ? chalk.gray("unranked (unverified)")
222
+ : spread >= 5 ? `${exAbbr(s.shortExchange)}>${exAbbr(s.longExchange)}${s.executable ? "" : chalk.gray(s.readOnlyLegs.length > 0 ? " (read-only)" : " (unverified)")}` : chalk.gray("-"),
295
223
  formatAvgRate(bestRate?.historicalAvg?.avg8h), formatAvgRate(bestRate?.historicalAvg?.avg24h), formatAvgRate(bestRate?.historicalAvg?.avg7d),
296
224
  ];
297
225
  });
298
- console.log(makeTable(["Symbol", ...columns.map(v => v.displayName), "Spread", "24h", "Direction", "Avg 8h", "Avg 24h", "Avg 7d"], rows));
226
+ console.log(makeTable(["Symbol", ...columns.map(v => v.apiStatus.kind === "unofficial" ? `${v.displayName}*` : v.displayName), "Spread", "24h", "Direction", "Avg 8h", "Avg 24h", "Avg 7d"], rows));
299
227
  console.log(chalk.gray(`\n ${snapshot.symbols.length} symbols compared across exchanges.\n`));
228
+ const unofficial = columns.filter(v => v.apiStatus.kind === "unofficial");
229
+ if (unofficial.length > 0)
230
+ console.log(chalk.yellow(` * ${unofficial.map(v => v.displayName).join(", ")}: UNOFFICIAL API (undocumented, read-only; may change or break). Unverified funding — shown only with --include-unverified.\n`));
300
231
  }
301
232
  function printFundingExchangeStatus(snapshot) {
302
233
  const statuses = Object.entries(snapshot.exchangeStatus).map(([ex, status]) => {
@@ -315,9 +246,16 @@ function printDetailedComparison(comparison) {
315
246
  const color = r.fundingRate > 0 ? chalk.red : r.fundingRate < 0 ? chalk.green : chalk.white;
316
247
  console.log(` ${chalk.white.bold(exAbbr(r.exchange).padEnd(4))} Raw: ${color(formatPercent(r.fundingRate).padEnd(14))} Hourly: ${(r.hourlyRate * 100).toFixed(6)}% Annual: ${r.annualizedPct.toFixed(2)}% APY: ${(compoundedReturn * 100).toFixed(2)}%`);
317
248
  }
249
+ if (!isRankedComparison(comparison)) {
250
+ console.log(chalk.yellow(`\n No ranked pair: fewer than two venues with verified funding (${comparison.unverifiedLegs.join(", ")} unverified — display only, never a long / short leg).\n`));
251
+ return;
252
+ }
318
253
  const spreadColor = comparison.maxSpreadAnnual >= 30 ? chalk.green.bold : comparison.maxSpreadAnnual >= 10 ? chalk.yellow : chalk.white;
319
254
  console.log(`\n Max Spread: ${spreadColor(`${comparison.maxSpreadAnnual.toFixed(1)}%`)} annual`);
320
- console.log(` Direction: Long ${exAbbr(comparison.longExchange)} / Short ${exAbbr(comparison.shortExchange)}`);
255
+ console.log(` Direction: Long ${exAbbr(comparison.longExchange)} / Short ${exAbbr(comparison.shortExchange)}${comparison.executable ? "" : chalk.yellow(` — not executable: ${[
256
+ ...comparison.readOnlyLegs.map(v => `${v} is read-only in perp-cli`),
257
+ ...comparison.unverifiedLegs.map(v => `${v} funding is unverified (display only)`),
258
+ ].join("; ")}`)}`);
321
259
  console.log(` Est. Income: $${comparison.estHourlyIncomeUsd.toFixed(4)}/hr per $1K notional\n`);
322
260
  }
323
261
  // registerFundingSubcommands removed — use 'arb scan --rates/--compare/--history/--positions' instead
@@ -325,9 +263,13 @@ function printDetailedComparison(comparison) {
325
263
  /** One-line stderr notice when rows of unverified-funding venues are shown (--include-unverified). */
326
264
  function warnUnverifiedFunding(rates) {
327
265
  const venues = [...new Set(rates.filter(r => !r.fundingVerified).map(r => r.exchange))];
328
- if (venues.length === 0)
329
- return;
330
- process.stderr.write(`[warn] ${venues.join(", ")}: funding rates are UNVERIFIED (unit not confirmed against funding payments; annualized values overstate venues whose funding accrues only during market hours). Shown on request — not for trading decisions.\n`);
266
+ if (venues.length > 0) {
267
+ process.stderr.write(`[warn] ${venues.join(", ")}: funding rates are UNVERIFIED (unit not confirmed against funding payments; annualized values overstate venues whose funding accrues only during market hours). Shown on request — not for trading decisions.\n`);
268
+ }
269
+ const unofficial = [...new Set(rates.filter(r => r.apiStatus === "unofficial").map(r => r.exchange))];
270
+ if (unofficial.length > 0) {
271
+ process.stderr.write(`[warn] ${unofficial.join(", ")}: read through an UNOFFICIAL API (undocumented; may change or be blocked without notice). Rows carry apiStatus "unofficial".\n`);
272
+ }
331
273
  }
332
274
  export async function handleRates(isJson, opts) {
333
275
  const minSpread = parseFloat(opts.minSpread);
@@ -366,66 +308,64 @@ export async function handleRates(isJson, opts) {
366
308
  const history = getHistoricalRates(s.symbol, bestEx, h24ago, now);
367
309
  return { ...s, rateHistory: history.map(h => ({ ts: h.ts, hourlyRate: h.hourlyRate })) };
368
310
  });
311
+ // --min never hides rows silently: the threshold and the hidden count are in meta (and data).
312
+ const minMeta = { minSpread: snapshot.minSpread, belowMinSpread: snapshot.belowMinSpread };
369
313
  if (process.argv.includes("--ndjson") || process.argv.includes("--fields"))
370
- return printJson(jsonOk(enriched));
371
- return printJson(jsonOk({ ...snapshot, symbols: enriched }));
314
+ return printJson(jsonOk(enriched, minMeta));
315
+ return printJson(jsonOk({ ...snapshot, symbols: enriched }, minMeta));
372
316
  }
373
317
  printSnapshotTable(snapshot);
374
318
  printFundingExchangeStatus(snapshot);
319
+ if (snapshot.belowMinSpread > 0) {
320
+ console.log(chalk.gray(` ${snapshot.belowMinSpread} pair${snapshot.belowMinSpread === 1 ? "" : "s"} below --min ${snapshot.minSpread}% hidden (show them: --min 0)\n`));
321
+ }
375
322
  }
323
+ /**
324
+ * Cross-venue price basis — a PRICE comparison, independent of funding.
325
+ *
326
+ * Venue scope: every venue with the "trading" role (both legs must be executable), whether or not
327
+ * its funding rate is verified; market-data-only venues (TxFlow, unofficial API) are out. Prices
328
+ * come from each venue's market directory and are grouped by `canonical` symbol with the funding
329
+ * scan's guards (fetchCanonicalPriceGroups): unclassified / ambiguous tickers have no canonical and
330
+ * never pair, and a venue whose mark deviates more than MAX_PRICE_DEVIATION_PCT from the group
331
+ * median is dropped. The chosen cheap / rich pair must itself agree within MAX_PRICE_DEVIATION_PCT:
332
+ * a wider gap is a different asset under the same ticker (BB on Lighter 8.50 vs Aster 0.0091).
333
+ * A venue that fails is reported on stderr and is absent from the scan, never silently empty.
334
+ */
376
335
  export async function handleBasisScan(isJson, opts) {
377
336
  if (!isJson())
378
337
  console.log(chalk.cyan(" Fetching prices for basis calculation...\n"));
379
- // SSOT rule #2: each DEX is independent. allSettled keeps the comparison
380
- // alive when one DEX errors out, while rejection reasons get logged to
381
- // stderr (explicit failure, never silent [] downgrade).
382
- const settled = await Promise.allSettled([
383
- fetchPacificaRates(),
384
- fetchHyperliquidRates(),
385
- fetchLighterRates(),
386
- fetchAsterRates(),
387
- ]);
388
- logSettledRejections(settled, ["pacifica", "hyperliquid", "lighter", "aster"], "basis-scan");
389
- const pacRates = settled[0].status === "fulfilled" ? settled[0].value : [];
390
- const hlRates = settled[1].status === "fulfilled" ? settled[1].value : [];
391
- const ltRates = settled[2].status === "fulfilled" ? settled[2].value : [];
392
- const astRates = settled[3].status === "fulfilled" ? settled[3].value : [];
393
- const exchangePrices = new Map();
394
338
  const filterSymbol = opts.symbol?.toUpperCase();
395
- for (const r of [...pacRates, ...hlRates, ...ltRates, ...astRates]) {
396
- if (r.markPrice <= 0)
397
- continue;
398
- if (filterSymbol && r.symbol.toUpperCase() !== filterSymbol)
399
- continue;
400
- if (!exchangePrices.has(r.symbol))
401
- exchangePrices.set(r.symbol, new Map());
402
- exchangePrices.get(r.symbol).set(r.exchange, r.markPrice);
403
- }
339
+ const snapshot = await fetchCanonicalPriceGroups(listVenuesWithRole("trading"), {
340
+ symbols: filterSymbol !== undefined ? [filterSymbol] : undefined, tag: "basis-scan",
341
+ });
342
+ for (const [venue, msg] of Object.entries(snapshot.exchangeErrors))
343
+ console.error(`[basis-scan] ${venue} fetch failed: ${msg}`);
404
344
  const opps = [];
405
- for (const [symbol, prices] of exchangePrices.entries()) {
406
- if (prices.size < 2)
345
+ for (const [symbol, priced] of snapshot.groups) {
346
+ if (priced.length < 2)
407
347
  continue;
408
- let minEx = "", maxEx = "", minPrice = Infinity, maxPrice = 0;
409
- for (const [ex, price] of prices) {
410
- if (price < minPrice) {
411
- minPrice = price;
412
- minEx = ex;
413
- }
414
- if (price > maxPrice) {
415
- maxPrice = price;
416
- maxEx = ex;
417
- }
348
+ let min = priced[0], max = priced[0];
349
+ for (const r of priced) {
350
+ if (r.markPrice < min.markPrice)
351
+ min = r;
352
+ if (r.markPrice > max.markPrice)
353
+ max = r;
418
354
  }
419
- if (minEx === maxEx)
355
+ if (min.exchange === max.exchange)
420
356
  continue;
421
- const diff = maxPrice - minPrice;
422
- const avgPrice = (maxPrice + minPrice) / 2;
357
+ const diff = max.markPrice - min.markPrice;
358
+ const avgPrice = (max.markPrice + min.markPrice) / 2;
423
359
  const pctDiff = (diff / avgPrice) * 100;
360
+ if (!(pctDiff <= MAX_PRICE_DEVIATION_PCT)) {
361
+ console.error(`[basis-scan] excluded ${symbol} ${min.exchange}↔${max.exchange}: mark gap ${pctDiff.toFixed(1)}% > ${MAX_PRICE_DEVIATION_PCT}% (likely a different asset with the same ticker)`);
362
+ continue;
363
+ }
424
364
  if (pctDiff * 365 >= parseFloat(opts.minBasis)) {
425
365
  const priceObj = {};
426
- for (const [ex, p] of prices)
427
- priceObj[ex] = p;
428
- opps.push({ symbol, prices: priceObj, priceDiff: diff, pctDiff, buyOn: minEx, sellOn: maxEx });
366
+ for (const r of priced)
367
+ priceObj[r.exchange] = r.markPrice;
368
+ opps.push({ symbol, prices: priceObj, priceDiff: diff, pctDiff, buyOn: min.exchange, sellOn: max.exchange });
429
369
  }
430
370
  }
431
371
  opps.sort((a, b) => b.pctDiff - a.pctDiff);
@@ -439,12 +379,12 @@ export async function handleBasisScan(isJson, opts) {
439
379
  console.log(chalk.gray(" Strategy: Buy cheap, sell expensive, profit from convergence.\n"));
440
380
  const rows = opps.map((o) => [
441
381
  chalk.white.bold(o.symbol),
442
- o.prices.pacifica ? `$${formatUsd(o.prices.pacifica)}` : chalk.gray("-"),
443
- o.prices.hyperliquid ? `$${formatUsd(o.prices.hyperliquid)}` : chalk.gray("-"),
444
- o.prices.lighter ? `$${formatUsd(o.prices.lighter)}` : chalk.gray("-"),
382
+ `$${formatUsd(o.prices[o.buyOn])}`,
383
+ `$${formatUsd(o.prices[o.sellOn])}`,
445
384
  `$${formatUsd(o.priceDiff)}`, `${o.pctDiff.toFixed(4)}%`, chalk.green(o.buyOn), chalk.red(o.sellOn),
385
+ chalk.gray(String(Object.keys(o.prices).length)),
446
386
  ]);
447
- console.log(makeTable(["Symbol", "Pacifica", "Hyperliquid", "Lighter", "Diff", "Basis %", "Buy On", "Sell On"], rows));
387
+ console.log(makeTable(["Symbol", "Buy Price", "Sell Price", "Diff", "Basis %", "Buy On", "Sell On", "Venues"], rows));
448
388
  }
449
389
  export async function handleDexScan(isJson, opts) {
450
390
  if (!isJson())
@@ -497,9 +437,13 @@ export async function handleCompare(isJson, symbol, includeUnverified = false) {
497
437
  if (comparison)
498
438
  warnUnverifiedFunding(comparison.rates);
499
439
  if (!comparison) {
440
+ // A venue ticker whose canonical symbol differs (Variational US500-PERP = SPY): say where to look.
441
+ const hint = (await import("../exchanges/variational/public.js")).canonicalHintFor(symbol);
500
442
  if (isJson())
501
- return printJson(jsonOk({ symbol: symbol.toUpperCase(), available: false }));
443
+ return printJson(jsonOk({ symbol: symbol.toUpperCase(), available: false, ...(hint !== null ? { hint } : {}) }));
502
444
  console.log(chalk.gray(` ${symbol.toUpperCase()} not found on at least 2 exchanges.\n`));
445
+ if (hint !== null)
446
+ console.log(chalk.yellow(` Hint: ${hint}\n`));
503
447
  return;
504
448
  }
505
449
  if (isJson())
@@ -594,7 +538,7 @@ export async function handleFundingPositions(isJson, getAdapterForExchange, opts
594
538
  }
595
539
  const sz = Math.abs(Number(pos.size));
596
540
  const nu = sz * mk;
597
- const fh = fd ? fd.fundingHours : null; /* market basis first (§10.1) */
541
+ const fh = fd ? fd.fundingHours : null; /* market basis first */
598
542
  const hr = fh !== null ? fr / fh : toHourlyRate(fr, exchange);
599
543
  const ap = annualizeHourlyRate(hr);
600
544
  const hp = estimateHourlyFundingFromHourly(hr, nu, pos.side);
@@ -1,5 +1,6 @@
1
1
  import { printJson, jsonOk, jsonError, makeTable, formatUsd } from "../utils.js";
2
2
  import { getHistoricalRates } from "../funding-history.js";
3
+ import { HyperliquidAdapter, hlEarliestCandleStart } from "../exchanges/hyperliquid.js";
3
4
  import chalk from "chalk";
4
5
  export function registerBacktestCommands(program, isJson) {
5
6
  const backtest = program.command("backtest").description("Backtest trading strategies on historical data");
@@ -32,10 +33,13 @@ export function registerBacktestCommands(program, isJson) {
32
33
  const ratesA = getHistoricalRates(sym, exchA, startTime, endTime);
33
34
  const ratesB = getHistoricalRates(sym, exchB, startTime, endTime);
34
35
  if (ratesA.length === 0 && ratesB.length === 0) {
36
+ // The funding history is written by `arb scan --rates` (saveFundingSnapshot; verified venues
37
+ // only). There is no `perp funding snapshot` command (0.15.0 named one).
38
+ const collect = `perp arb scan --rates --symbol ${sym}`;
35
39
  if (isJson())
36
- return printJson(jsonError("NO_DATA", `No historical funding data for ${sym}. Run 'perp funding snapshot' first to collect data.`));
40
+ return printJson(jsonError("NO_DATA", `No historical funding data for ${sym}. Run '${collect}' periodically to collect data first.`));
37
41
  console.log(chalk.yellow(`\n No historical funding data for ${sym} on ${exchA}/${exchB}.`));
38
- console.log(chalk.yellow(` Run 'perp funding snapshot' periodically to collect data first.\n`));
42
+ console.log(chalk.yellow(` Run '${collect}' periodically to collect data first.\n`));
39
43
  return;
40
44
  }
41
45
  // Build time-aligned rate pairs
@@ -180,31 +184,25 @@ export function registerBacktestCommands(program, isJson) {
180
184
  console.error(chalk.red("Error: Need at least 2 grid lines"));
181
185
  process.exit(1);
182
186
  }
187
+ // Hyperliquid serves only the most recent 5000 candles per interval (docs "Candle snapshot");
188
+ // with 1h candles a --days above ~208 cannot be read. The start is clamped to the available
189
+ // window (one hour inside it) and the result says so — never presented as the full period.
190
+ const DAY_MS = 24 * 60 * 60 * 1000;
183
191
  const endTime = Date.now();
184
- const startTime = endTime - days * 24 * 60 * 60 * 1000;
185
- // Fetch historical klines from Hyperliquid
192
+ const requestedStart = endTime - days * DAY_MS;
193
+ const earliest = hlEarliestCandleStart("1h", endTime) + 60 * 60 * 1000;
194
+ const truncated = requestedStart < earliest;
195
+ const startTime = truncated ? earliest : requestedStart;
196
+ const dataFrom = startTime;
197
+ const coveredDays = (endTime - startTime) / DAY_MS;
198
+ // Fetch historical klines from Hyperliquid (adapter: symbol resolution, interval / window checks)
186
199
  if (!isJson()) {
187
200
  console.log(chalk.gray(`\n Fetching ${days}d of 1h klines for ${sym}...`));
188
201
  }
189
202
  let klines;
190
203
  try {
191
- const resp = await fetch("https://api.hyperliquid.xyz/info", {
192
- method: "POST",
193
- headers: { "Content-Type": "application/json" },
194
- body: JSON.stringify({
195
- type: "candleSnapshot",
196
- req: {
197
- coin: sym,
198
- interval: "1h",
199
- startTime,
200
- endTime,
201
- },
202
- }),
203
- });
204
- if (!resp.ok)
205
- throw new Error(`HTTP ${resp.status}`);
206
- const data = await resp.json();
207
- klines = data.map(k => ({ t: k.t, o: k.o, h: k.h, l: k.l, c: k.c }));
204
+ const rows = await new HyperliquidAdapter(undefined, false).getKlines(sym, "1h", startTime, endTime);
205
+ klines = rows.map(k => ({ t: k.time, o: k.open, h: k.high, l: k.low, c: k.close }));
208
206
  }
209
207
  catch (err) {
210
208
  const msg = err instanceof Error ? err.message : String(err);
@@ -291,6 +289,8 @@ export function registerBacktestCommands(program, isJson) {
291
289
  symbol: sym,
292
290
  period: `${days} days`,
293
291
  klines: klines.length,
292
+ dataFrom: new Date(dataFrom).toISOString(),
293
+ truncated,
294
294
  priceRange,
295
295
  gridRange: `$${formatUsd(String(lowerPrice))} - $${formatUsd(String(upperPrice))}`,
296
296
  grids,
@@ -307,6 +307,8 @@ export function registerBacktestCommands(program, isJson) {
307
307
  return printJson(jsonOk(summary));
308
308
  console.log(chalk.cyan.bold(`\n Grid Backtest — ${sym}\n`));
309
309
  console.log(` Period: ${days} days (${klines.length} candles)`);
310
+ if (truncated)
311
+ console.log(chalk.yellow(` Truncated: Hyperliquid serves only the most recent 5000 1h candles — start clamped to ${new Date(dataFrom).toISOString()} (${coveredDays.toFixed(1)} of ${days} days)`));
310
312
  console.log(` Price range: ${priceRange}`);
311
313
  console.log(` Grid range: $${formatUsd(String(lowerPrice))} - $${formatUsd(String(upperPrice))}`);
312
314
  console.log(` Grid lines: ${grids} (step: $${step.toFixed(2)})`);
@@ -419,7 +419,7 @@ export function registerStrategyCommands(program, getAdapter, getAdapterFor, isJ
419
419
  }
420
420
  console.log();
421
421
  });
422
- // ── quarantine (design §12.3): positions the bot refuses to touch until a human clears them ──
422
+ // ── quarantine: positions the bot refuses to touch until a human clears them ──
423
423
  bot
424
424
  .command("quarantine")
425
425
  .description("List quarantined symbols (unknown / unverified order outcome or unhedged leg) — blocked for every bot strategy, arb exec and arb auto (~/.perp/bot-quarantine.json)")
@@ -283,7 +283,7 @@ export function registerWalletBuilderCommands(wallet, isJson, getAdapterForExcha
283
283
  .action(async (venueArg, _opts, cmd) => {
284
284
  await withJsonErrors(isJson(), async () => {
285
285
  const network = networkOf(cmd);
286
- const venues = venueArg !== undefined ? [getVenue(venueArg).id] : [...builderFeeVenues(), ...["lighter", "lighter-rh", "arcus"]];
286
+ const venues = venueArg !== undefined ? [getVenue(venueArg).id] : [...builderFeeVenues(), ...["lighter", "lighter-rh", "arcus", "risex", "txflow", "variational", "grvt", "extended"]];
287
287
  const rows = [];
288
288
  for (const v of venues) {
289
289
  const configured = builderTargetOf(loadSettings().builder, v) !== null;