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,6 +1,6 @@
1
1
  /**
2
2
  * Public market data for the legacy venues without a native `getVenueMarkets`
3
- * (pacifica, hyperliquid core, aster) — design §10.6. Lighter uses
3
+ * (pacifica, hyperliquid core, aster). Lighter uses
4
4
  * exchanges/lighter-public.ts.
5
5
  *
6
6
  * Network-aware: the REST base comes from `legacyPublicBaseUrl(venue, net)`,
@@ -19,23 +19,26 @@
19
19
  * pacifica: crypto-centric; PACIFICA_NON_CRYPTO lists every non-crypto perp
20
20
  * listed on 2026-09-30.
21
21
  * aster: exchangeInfo `underlyingSubType` (STOCK / ETF / Commodities / pre-launch).
22
- * AMBIGUOUS tickers are excluded with a warning so they cannot produce a false
23
- * cross-venue match (SPX = the SPX6900 token, whose ticker equals the S&P 500
24
- * canonical key).
22
+ * Ambiguous tickers (UNCLASSIFIED_TICKERS, src/ws/unclassified.ts — SPX = the SPX6900
23
+ * token, whose ticker equals the S&P 500 canonical key) are listed as "unclassified"
24
+ * (canonical null) so they cannot produce a false cross-venue match.
25
25
  *
26
26
  * Fields a venue does not publish on this path are null (HL has no fixed tick
27
27
  * size; Aster leverage brackets need a signed call; Aster OI is per-symbol only).
28
28
  */
29
29
  import { HL_MIN_ORDER_VALUE_USD } from "../constants.js";
30
+ import { isPacificaPerpetual } from "../ws/pacifica-instrument.js";
31
+ import { asterCliSymbol, asterMatchable, isAsterListedPerpetual } from "../ws/aster-market.js";
32
+ import { isUnclassifiedTicker } from "../ws/unclassified.js";
30
33
  import { PerpError } from "../errors.js";
31
34
  import { venueRateString } from "../utils/numeric.js";
32
35
  import { assertOk } from "../api/public/_http.js";
33
36
  import { legacyPublicBaseUrl } from "../api/public/urls.js";
34
37
  import { canonicalFor } from "../exchanges/canonical-symbols.js";
35
38
  import { fundingBasisOf, settlementIntervalOf } from "../exchanges/venue.js";
39
+ import { nextTopOfHourMs } from "../exchanges/venue-types.js";
36
40
  // ── Classification ──
37
- const AMBIGUOUS = new Set(["SPX"]);
38
- /** Pacifica non-crypto perps (live /info 2026-09-30). BP / CHIP: ticker collides with a listed equity/ETF. */
41
+ /** Pacifica non-crypto perps (live /info 2026-09-30). Ambiguous BP / CHIP / SPX: UNCLASSIFIED_TICKERS. */
39
42
  const PACIFICA_NON_CRYPTO = {
40
43
  NVDA: "equity", TSLA: "equity", GOOGL: "equity", PLTR: "equity", HOOD: "equity", CRCL: "equity",
41
44
  SPCX: "equity", SKHYNIX: "equity", SAMSUNG: "equity", MSTR: "equity", MU: "equity", SNDK: "equity",
@@ -46,19 +49,20 @@ const PACIFICA_NON_CRYPTO = {
46
49
  USDJPY: "fx", EURUSD: "fx",
47
50
  SP500: "index",
48
51
  };
49
- const PACIFICA_AMBIGUOUS = new Set(["BP", "CHIP"]);
52
+ // Ambiguous tickers are listed as "unclassified" (canonical null: never matched across venues),
53
+ // so every listing path (CLI, MCP, public directory) shows the same market set.
50
54
  function classifyPacifica(symbol) {
51
55
  const up = symbol.toUpperCase();
52
- if (AMBIGUOUS.has(up) || PACIFICA_AMBIGUOUS.has(up))
53
- return null;
56
+ if (isUnclassifiedTicker("pacifica", up))
57
+ return "unclassified";
54
58
  return PACIFICA_NON_CRYPTO[up] ?? "crypto"; // crypto-centric venue: non-crypto is listed explicitly
55
59
  }
56
60
  function classifyHyperliquid(symbol) {
57
- return AMBIGUOUS.has(symbol.toUpperCase()) ? null : "crypto";
61
+ return isUnclassifiedTicker("hyperliquid", symbol) ? "unclassified" : "crypto";
58
62
  }
59
63
  function classifyAster(symbol, subTypes) {
60
- if (AMBIGUOUS.has(symbol.toUpperCase()))
61
- return null;
64
+ if (isUnclassifiedTicker("aster", symbol))
65
+ return "unclassified";
62
66
  if (subTypes.includes("pre-launch"))
63
67
  return "preIpo";
64
68
  if (subTypes.includes("ETF"))
@@ -75,6 +79,10 @@ function warnExcluded(venue, what, symbols) {
75
79
  }
76
80
  }
77
81
  // ── HTTP ──
82
+ /** One public JSON read with the venue error mapping (rate limits, HTTP status, invalid JSON). */
83
+ export async function legacyPublicJson(venue, url, label, init) {
84
+ return getJson(venue, url, label, init);
85
+ }
78
86
  async function getJson(venue, url, label, init) {
79
87
  const res = await fetch(url, { ...init, signal: AbortSignal.timeout(15_000) });
80
88
  await assertOk(res, `${venue} ${label}`);
@@ -124,21 +132,16 @@ async function pacificaRows(net) {
124
132
  const basis = fundingBasisOf("pacifica");
125
133
  const settlement = settlementIntervalOf("pacifica");
126
134
  const out = [];
127
- const unclassified = [];
128
135
  const unpriced = [];
129
136
  for (const m of info) {
130
- // /info lists spot markets too (e.g. SOL-USDC). A row without the field cannot be classified.
137
+ // /info lists spot markets too (e.g. SOL-USDC): perps only. A row without the field cannot be classified.
131
138
  if (typeof m.instrument_type !== "string") {
132
139
  throw new PerpError("EXCHANGE_ERROR", `Pacifica /info row ${String(m.symbol)} has no instrument_type`, { exchange: "pacifica" });
133
140
  }
134
- if (m.instrument_type === "spot")
141
+ if (!isPacificaPerpetual(m))
135
142
  continue;
136
143
  const symbol = String(m.symbol ?? "");
137
144
  const assetClass = classifyPacifica(symbol);
138
- if (assetClass === null) {
139
- unclassified.push(symbol);
140
- continue;
141
- }
142
145
  const p = priceBySymbol.get(symbol);
143
146
  const mark = p ? positiveNumStr(p.mark) : null;
144
147
  if (!p || mark === null) {
@@ -157,7 +160,9 @@ async function pacificaRows(net) {
157
160
  quoteAsset: null,
158
161
  markPrice: mark,
159
162
  indexPrice: positiveNumStr(p.oracle),
160
- fundingRate: rateStr(p.next_funding),
163
+ // Last settled hourly rate (WS prices `funding`, the CLI row's fundingRate); next_funding
164
+ // is the predicted one.
165
+ fundingRate: rateStr(p.funding),
161
166
  fundingRateBasis: basis,
162
167
  settlementIntervalHours: settlement,
163
168
  openInterest: numStr(p.open_interest),
@@ -171,10 +176,9 @@ async function pacificaRows(net) {
171
176
  session: null,
172
177
  tradable: true,
173
178
  },
174
- nextFundingTime: null, // Pacifica publishes no next-funding timestamp
179
+ nextFundingTime: nextTopOfHourMs(), // not published; docs: settled at the end of each 1 h interval (on the hour)
175
180
  });
176
181
  }
177
- warnExcluded("pacifica", "unclassified/ambiguous market(s)", unclassified);
178
182
  warnExcluded("pacifica", "market(s) without a mark price", unpriced);
179
183
  return out;
180
184
  }
@@ -197,16 +201,15 @@ async function hyperliquidRows(net) {
197
201
  const basis = fundingBasisOf("hyperliquid");
198
202
  const settlement = settlementIntervalOf("hyperliquid");
199
203
  const out = [];
200
- const unclassified = [];
201
204
  const unpriced = [];
202
205
  universe.forEach((a, i) => {
206
+ // Delisted assets stay in meta (isDelisted: true) with zeroed funding / volume / OI
207
+ // (live 2026-10-14: 56 of 234) — not a market: excluded from listings, prices and scans.
208
+ if (a.isDelisted === true)
209
+ return;
203
210
  const ctx = ctxs[i];
204
211
  const symbol = String(a.name ?? "");
205
212
  const assetClass = classifyHyperliquid(symbol);
206
- if (assetClass === null) {
207
- unclassified.push(symbol);
208
- return;
209
- }
210
213
  const mark = positiveNumStr(ctx.markPx);
211
214
  if (mark === null) {
212
215
  unpriced.push(symbol);
@@ -237,12 +240,11 @@ async function hyperliquidRows(net) {
237
240
  maxLeverage: Number.isFinite(maxLeverage) && maxLeverage > 0 ? maxLeverage : null,
238
241
  marginTiers: null,
239
242
  session: null,
240
- tradable: a.isDelisted !== true,
243
+ tradable: true, // delisted assets are skipped above
241
244
  },
242
- nextFundingTime: null, // not published in metaAndAssetCtxs
245
+ nextFundingTime: nextTopOfHourMs(), // not in metaAndAssetCtxs; docs: paid every hour (on the hour)
243
246
  });
244
247
  });
245
- warnExcluded("hyperliquid", "unclassified/ambiguous market(s)", unclassified);
246
248
  warnExcluded("hyperliquid", "market(s) without a mark price", unpriced);
247
249
  return out;
248
250
  }
@@ -271,20 +273,20 @@ async function asterRows(net) {
271
273
  intervalBySymbol.set(String(f.symbol), h);
272
274
  }
273
275
  const out = [];
274
- const unclassified = [];
275
276
  const unpriced = [];
276
277
  const noInterval = [];
277
278
  for (const s of symbols) {
278
- if (s.contractType !== "PERPETUAL" || s.quoteAsset !== "USDT")
279
+ // Shared eligibility (SETTLING / PENDING_TRADING rows were listed with tradable false); every
280
+ // quote, like the CLI adapter and the WS directory.
281
+ if (!isAsterListedPerpetual(s))
279
282
  continue;
280
283
  const venueSymbol = String(s.symbol);
281
- const symbol = String(s.baseAsset);
284
+ const symbol = asterCliSymbol({ symbol: venueSymbol, baseAsset: String(s.baseAsset), quoteAsset: s.quoteAsset });
285
+ const base = String(s.baseAsset);
282
286
  const subTypes = Array.isArray(s.underlyingSubType) ? s.underlyingSubType.map(String) : [];
283
- const assetClass = classifyAster(symbol, subTypes);
284
- if (assetClass === null) {
285
- unclassified.push(venueSymbol);
286
- continue;
287
- }
287
+ const assetClass = classifyAster(base, subTypes);
288
+ // A second listing of a base (BTCUSD1 beside BTCUSDT) or a non-USD quote (U): canonical null.
289
+ const matchable = asterMatchable(s);
288
290
  const p = premiumBySymbol.get(venueSymbol);
289
291
  const mark = p ? positiveNumStr(p.markPrice) : null;
290
292
  if (!p || mark === null) {
@@ -306,10 +308,10 @@ async function asterRows(net) {
306
308
  venue: "aster",
307
309
  venueSymbol,
308
310
  symbol,
309
- canonical: canonicalFor("aster", symbol, assetClass),
311
+ canonical: matchable ? canonicalFor("aster", base, assetClass) : null,
310
312
  assetClass,
311
313
  settle: s.marginAsset,
312
- quoteAsset: s.quoteAsset,
314
+ quoteAsset: typeof s.quoteAsset === "string" && s.quoteAsset !== "" ? s.quoteAsset : null,
313
315
  markPrice: mark,
314
316
  indexPrice: positiveNumStr(p.indexPrice),
315
317
  fundingRate: rateStr(p.lastFundingRate),
@@ -329,7 +331,6 @@ async function asterRows(net) {
329
331
  nextFundingTime: Number.isFinite(next) && next > 0 ? next : null,
330
332
  });
331
333
  }
332
- warnExcluded("aster", "unclassified/ambiguous market(s)", unclassified);
333
334
  warnExcluded("aster", "market(s) without a mark price", unpriced);
334
335
  warnExcluded("aster", "market(s) missing from fundingInfo", noInterval);
335
336
  return out;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Funding rate normalization (design §10.1, §10.13 #1).
2
+ * Funding rate normalization.
3
3
  *
4
4
  * The number of hours one quoted rate covers (the funding BASIS) comes from the
5
5
  * venue definition — `VenueDefinition.fundingRateBasis`, read through
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Funding rate normalization (design §10.1, §10.13 #1).
2
+ * Funding rate normalization.
3
3
  *
4
4
  * The number of hours one quoted rate covers (the funding BASIS) comes from the
5
5
  * venue definition — `VenueDefinition.fundingRateBasis`, read through
@@ -1,17 +1,18 @@
1
1
  /**
2
2
  * Real-time cross-venue funding rate comparison.
3
3
  *
4
- * Fans out over every venue with the "marketData" role (design §10.6) using
4
+ * Fans out over every venue with the "marketData" role using
5
5
  * each venue's `publicData.fetchFundingRates(net)`, normalizes every row to a
6
6
  * per-hour rate with the row's own basis (Lighter
7
7
  * family 8h, Aster per-symbol), and matches markets across venues by
8
- * `canonical` (null → excluded, §10.9).
8
+ * `canonical` (null → excluded).
9
9
  *
10
10
  * A venue that fails is reported in `exchangeStatus` ("error") and
11
11
  * `exchangeErrors` (message) — its rows are absent, never silently empty.
12
12
  */
13
13
  import { type HistoricalAverages } from "./history.js";
14
14
  import { type VenueDefinition } from "../exchanges/venue.js";
15
+ import type { FundingRateKind } from "../exchanges/interface.js";
15
16
  import { type FundingRateBasis, type Network, type VenueId } from "../exchanges/venue-types.js";
16
17
  export interface ExchangeFundingRate {
17
18
  exchange: VenueId;
@@ -28,22 +29,65 @@ export interface ExchangeFundingRate {
28
29
  historicalAvg: HistoricalAverages | null;
29
30
  /** false = the venue's funding rate is unverified (VenueDefinition.fundingVerified) — shown only on explicit request. */
30
31
  fundingVerified: boolean;
32
+ /** How the row was read: "unofficial" = an undocumented venue API (VenueDefinition.apiStatus) — may change without notice. */
33
+ apiStatus: "official" | "unofficial";
34
+ /**
35
+ * Which rate `fundingRate` is (the venue market row's rate — arb compares those, never a
36
+ * predicted next-period rate unless the venue only publishes that): "lastSettled" | "accruing" |
37
+ * "predicted", null = not verified (fundingRateKindOf).
38
+ */
39
+ rateKind: FundingRateKind | null;
31
40
  }
32
41
  export interface SymbolFundingComparison {
33
42
  /** Canonical symbol shared by every row. */
34
43
  symbol: string;
35
44
  rates: ExchangeFundingRate[];
45
+ /**
46
+ * Ranking is over VERIFIED rows only (VenueDefinition.fundingVerified): an unverified row
47
+ * (`--include-unverified`) is shown in `rates` but is never the long or short leg nor part of
48
+ * the spread. Fewer than two verified rows → no ranked pair: longExchange / shortExchange /
49
+ * maxSpreadAnnual / estHourlyIncomeUsd are null (a display-only row).
50
+ */
51
+ maxSpreadAnnual: number | null;
52
+ longExchange: string | null;
53
+ shortExchange: string | null;
54
+ bestMarkPrice: number;
55
+ estHourlyIncomeUsd: number | null;
56
+ /**
57
+ * false = a leg of the long/short pair sits on a venue without the trading role (RISEx while its
58
+ * trading is gated in this release): the spread is data only — `arb exec` refuses it.
59
+ */
60
+ executable: boolean;
61
+ /**
62
+ * true = the row's rates are of different kinds (e.g. Lighter lastSettled vs Hyperliquid
63
+ * accruing) — the perp-ws comparator's `mixedKinds`: the spread compares unlike quantities.
64
+ */
65
+ mixedKinds: boolean;
66
+ /** Venues of the long/short pair that perp-cli cannot trade (read-only); [] when executable. */
67
+ readOnlyLegs: string[];
68
+ /**
69
+ * Venues of the long/short pair whose funding is unverified (VenueDefinition.fundingVerified
70
+ * false, e.g. QFEX — shown only with --include-unverified, display only); such a pair is never
71
+ * executable. [] when both legs are verified.
72
+ */
73
+ unverifiedLegs: string[];
74
+ }
75
+ /** A comparison with a ranked pair (≥ 2 verified rows): long / short / spread / income are set. */
76
+ export type RankedFundingComparison = SymbolFundingComparison & {
36
77
  maxSpreadAnnual: number;
37
78
  longExchange: string;
38
79
  shortExchange: string;
39
- bestMarkPrice: number;
40
80
  estHourlyIncomeUsd: number;
41
- }
81
+ };
82
+ export declare function isRankedComparison(c: SymbolFundingComparison): c is RankedFundingComparison;
42
83
  export interface FundingRateSnapshot {
43
84
  timestamp: string;
44
85
  symbols: SymbolFundingComparison[];
86
+ /** The minSpread applied (annualized %) and how many ranked pairs it hid (spread below it). */
87
+ minSpread: number;
88
+ belowMinSpread: number;
45
89
  exchangeStatus: Record<string, "ok" | "error">;
46
- /** Error message per venue whose status is "error" (§10.13 #3). */
90
+ /** Error message per venue whose status is "error". */
47
91
  exchangeErrors: Record<string, string>;
48
92
  }
49
93
  export declare const TOP_SYMBOLS: string[];
@@ -59,6 +103,51 @@ export declare function fetchVenuesRates(venues: VenueDefinition[], net: Network
59
103
  exchangeStatus: Record<string, "ok" | "error">;
60
104
  exchangeErrors: Record<string, string>;
61
105
  }>;
106
+ /**
107
+ * Same canonical ticker on two venues is not proof of the same asset (e.g. two
108
+ * unrelated tokens both listed as "PRL"). Within a canonical group, drop any
109
+ * venue whose mark price deviates more than MAX_PRICE_DEVIATION_PCT from the
110
+ * group median, with a one-time stderr warning per (symbol, venue).
111
+ */
112
+ export declare function dropPriceOutliers<T extends {
113
+ exchange: VenueId;
114
+ markPrice: number;
115
+ }>(canonical: string, list: T[], tag?: string): T[];
116
+ export interface VenuePrice {
117
+ exchange: VenueId;
118
+ /** Venue display symbol. */
119
+ symbol: string;
120
+ canonical: string;
121
+ markPrice: number;
122
+ apiStatus: "official" | "unofficial";
123
+ }
124
+ export interface PriceGroupSnapshot {
125
+ /** canonical → venue prices, after the median price-outlier drop (same guard as the funding scan). */
126
+ groups: Map<string, VenuePrice[]>;
127
+ exchangeStatus: Record<string, "ok" | "error">;
128
+ exchangeErrors: Record<string, string>;
129
+ }
130
+ /**
131
+ * Mark prices of `venues`, grouped by canonical symbol — the grouping and price-outlier guard of
132
+ * the funding comparison, WITHOUT any funding dependency: read from each venue's market directory
133
+ * (publicData.fetchMarkets), so a market with a null funding rate or a venue whose funding is
134
+ * unverified is still priced. Rows without a canonical symbol, not tradable, or without a
135
+ * positive mark are excluded. A venue that fails is reported in exchangeStatus / exchangeErrors.
136
+ */
137
+ export declare function fetchCanonicalPriceGroups(venues: VenueDefinition[], opts?: {
138
+ symbols?: string[];
139
+ network?: Network;
140
+ tag?: string;
141
+ }): Promise<PriceGroupSnapshot>;
142
+ /**
143
+ * Whether a long/short pair can be traded as a funding arb: both legs need the trading role AND
144
+ * verified funding (an unverified-funding venue is display only — never ranked for trading).
145
+ */
146
+ export declare function pairExecutability(longExchange: string, shortExchange: string): {
147
+ executable: boolean;
148
+ readOnlyLegs: string[];
149
+ unverifiedLegs: string[];
150
+ };
62
151
  /**
63
152
  * Fetch funding rates from every market-data venue in parallel, normalize, and compare.
64
153
  * Returns rates sorted by max spread (descending).
@@ -97,7 +186,7 @@ export interface SpotPerpSpread {
97
186
  /**
98
187
  * Fetch spot+perp funding rate spreads.
99
188
  * Since spot has 0 funding cost, the spread is simply |perp funding rate|.
100
- * Only includes symbols available on at least one bot spot venue (def.spot, §12.4 #3).
189
+ * Only includes symbols available on at least one bot spot venue (def.spot).
101
190
  *
102
191
  * Key safety checks:
103
192
  * - U-token mapping: UBTC→BTC, UETH→ETH, USOL→SOL, UFART→FARTCOIN
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Real-time cross-venue funding rate comparison.
3
3
  *
4
- * Fans out over every venue with the "marketData" role (design §10.6) using
4
+ * Fans out over every venue with the "marketData" role using
5
5
  * each venue's `publicData.fetchFundingRates(net)`, normalizes every row to a
6
6
  * per-hour rate with the row's own basis (Lighter
7
7
  * family 8h, Aster per-symbol), and matches markets across venues by
8
- * `canonical` (null → excluded, §10.9).
8
+ * `canonical` (null → excluded).
9
9
  *
10
10
  * A venue that fails is reported in `exchangeStatus` ("error") and
11
11
  * `exchangeErrors` (message) — its rows are absent, never silently empty.
@@ -14,10 +14,15 @@ import { annualizeHourlyRate, estimateHourlyFundingFromHourly } from "./normaliz
14
14
  import { getHistoricalAverages } from "./history.js";
15
15
  import { SPOT_PERP_TOKEN_MAP, MAX_PRICE_DEVIATION_PCT } from "../exchanges/index.js";
16
16
  import { getVenue, listVenuesWithRole, READ_ONLY_CREDENTIALS } from "../exchanges/venue.js";
17
- import { rateToHourly } from "../exchanges/venue-types.js";
17
+ import { fundingRateKindOf, rateToHourly } from "../exchanges/venue-types.js";
18
18
  import { ensureVenuesRegistered } from "../exchanges/venues/index.js";
19
19
  import { sharedApiNetwork } from "../api/public/urls.js";
20
20
  import { PerpError } from "../errors.js";
21
+ import { canonicalQuerySymbol } from "../exchanges/canonical-symbols.js";
22
+ export function isRankedComparison(c) {
23
+ // estHourlyIncomeUsd is set together with the pair (null exactly when the pair is).
24
+ return c.longExchange !== null && c.shortExchange !== null && c.maxSpreadAnnual !== null;
25
+ }
21
26
  // ── Default top symbols to track ──
22
27
  export const TOP_SYMBOLS = [
23
28
  "BTC", "ETH", "SOL", "DOGE", "SUI", "AVAX", "LINK", "ARB",
@@ -26,7 +31,11 @@ export const TOP_SYMBOLS = [
26
31
  ];
27
32
  /** Mark-price source preference for `bestMarkPrice` (most liquid first). */
28
33
  const MARK_PRICE_PREFERENCE = [
29
- "hyperliquid", "pacifica", "lighter", "aster", "lighter-rh", "ondo", "arcus", "qfex",
34
+ "hyperliquid", "pacifica", "lighter", "aster", "lighter-rh", "ondo", "arcus", "risex", "qfex", "grvt", "extended",
35
+ // RFQ venue: the mark is the single maker's (OLP) price.
36
+ "variational",
37
+ // Unofficial API: last resort as a mark-price source.
38
+ "txflow",
30
39
  ];
31
40
  // ── Per-venue fetch ──
32
41
  function errorMessage(e) {
@@ -51,20 +60,24 @@ async function fetchVenueRates(def, net) {
51
60
  for (const r of rows) {
52
61
  if (r.canonical === null)
53
62
  continue; // not matchable across venues
63
+ // Outside its funding hours (QFEX market_hours) a market has no rate: never ranked, snapshotted
64
+ // (saveFundingSnapshot) or averaged as a 0.
65
+ if (r.rate === null || r.marketClosed === true)
66
+ continue;
54
67
  const markRaw = r.markPrice !== null ? r.markPrice : markBySymbol?.get(r.venueSymbol);
55
68
  const markPrice = markRaw === undefined ? NaN : Number(markRaw);
56
69
  if (!Number.isFinite(markPrice) || markPrice <= 0) {
57
70
  unpriced.push(r.symbol);
58
71
  continue;
59
72
  }
60
- out.push(toExchangeRate(r, r.canonical, markPrice, def.fundingVerified));
73
+ out.push(toExchangeRate({ ...r, rate: r.rate }, r.canonical, markPrice, def.fundingVerified, def.apiStatus.kind));
61
74
  }
62
75
  if (unpriced.length > 0) {
63
76
  console.warn(`[${def.id}] excluded ${unpriced.length} funding row(s) without a mark price: ${unpriced.join(", ")}`);
64
77
  }
65
78
  return out;
66
79
  }
67
- function toExchangeRate(r, canonical, markPrice, fundingVerified) {
80
+ function toExchangeRate(r, canonical, markPrice, fundingVerified, apiStatus) {
68
81
  const fundingRate = Number(r.rate);
69
82
  if (!Number.isFinite(fundingRate)) {
70
83
  throw new PerpError("EXCHANGE_ERROR", `${r.venue} ${r.symbol}: non-numeric funding rate "${r.rate}"`);
@@ -82,6 +95,8 @@ function toExchangeRate(r, canonical, markPrice, fundingVerified) {
82
95
  nextFundingTime: r.nextFundingTime,
83
96
  historicalAvg: null,
84
97
  fundingVerified,
98
+ apiStatus,
99
+ rateKind: fundingRateKindOf(r.venue, r.rate),
85
100
  };
86
101
  }
87
102
  /**
@@ -117,7 +132,7 @@ const _warnedPriceMismatch = new Set();
117
132
  * venue whose mark price deviates more than MAX_PRICE_DEVIATION_PCT from the
118
133
  * group median, with a one-time stderr warning per (symbol, venue).
119
134
  */
120
- function dropPriceOutliers(canonical, list) {
135
+ export function dropPriceOutliers(canonical, list, tag = "funding") {
121
136
  if (list.length < 2)
122
137
  return list;
123
138
  const sorted = list.map(r => r.markPrice).sort((x, y) => x - y);
@@ -133,12 +148,12 @@ function dropPriceOutliers(canonical, list) {
133
148
  const key = `${canonical}:${r.exchange}`;
134
149
  if (!_warnedPriceMismatch.has(key)) {
135
150
  _warnedPriceMismatch.add(key);
136
- process.stderr.write(`[funding] excluded ${r.exchange} ${canonical}: mark ${r.markPrice} deviates ${devPct.toFixed(1)}% from cross-venue median ${mid} (likely a different asset with the same ticker)\n`);
151
+ process.stderr.write(`[${tag}] excluded ${r.exchange} ${canonical}: mark ${r.markPrice} deviates ${devPct.toFixed(1)}% from cross-venue median ${mid} (likely a different asset with the same ticker)\n`);
137
152
  }
138
153
  return false;
139
154
  });
140
155
  }
141
- function groupByCanonical(rates, symbols) {
156
+ function groupByCanonical(rates, symbols, tag = "funding") {
142
157
  const out = new Map();
143
158
  for (const r of rates) {
144
159
  if (symbols && !symbols.includes(r.canonical))
@@ -150,7 +165,7 @@ function groupByCanonical(rates, symbols) {
150
165
  out.set(r.canonical, [r]);
151
166
  }
152
167
  for (const [canonical, list] of [...out]) {
153
- const kept = dropPriceOutliers(canonical, list);
168
+ const kept = dropPriceOutliers(canonical, list, tag);
154
169
  if (kept.length === 0)
155
170
  out.delete(canonical); // every venue disagreed — not comparable
156
171
  else
@@ -158,6 +173,49 @@ function groupByCanonical(rates, symbols) {
158
173
  }
159
174
  return out;
160
175
  }
176
+ /**
177
+ * Mark prices of `venues`, grouped by canonical symbol — the grouping and price-outlier guard of
178
+ * the funding comparison, WITHOUT any funding dependency: read from each venue's market directory
179
+ * (publicData.fetchMarkets), so a market with a null funding rate or a venue whose funding is
180
+ * unverified is still priced. Rows without a canonical symbol, not tradable, or without a
181
+ * positive mark are excluded. A venue that fails is reported in exchangeStatus / exchangeErrors.
182
+ */
183
+ export async function fetchCanonicalPriceGroups(venues, opts) {
184
+ ensureVenuesRegistered();
185
+ const net = opts?.network ?? sharedApiNetwork();
186
+ const settled = await Promise.allSettled(venues.map(v => v.publicData.fetchMarkets(net, { funding: false })));
187
+ const exchangeStatus = {};
188
+ const exchangeErrors = {};
189
+ const prices = [];
190
+ settled.forEach((res, i) => {
191
+ const def = venues[i];
192
+ if (res.status === "rejected") {
193
+ exchangeStatus[def.id] = "error";
194
+ exchangeErrors[def.id] = errorMessage(res.reason);
195
+ return;
196
+ }
197
+ exchangeStatus[def.id] = "ok";
198
+ for (const m of res.value) {
199
+ if (m.canonical === null || !m.tradable)
200
+ continue;
201
+ const markPrice = Number(m.markPrice);
202
+ if (!Number.isFinite(markPrice) || markPrice <= 0)
203
+ continue;
204
+ prices.push({ exchange: def.id, symbol: m.symbol, canonical: m.canonical, markPrice, apiStatus: def.apiStatus.kind });
205
+ }
206
+ });
207
+ return { groups: groupByCanonical(prices, opts?.symbols, opts?.tag ?? "prices"), exchangeStatus, exchangeErrors };
208
+ }
209
+ /**
210
+ * Whether a long/short pair can be traded as a funding arb: both legs need the trading role AND
211
+ * verified funding (an unverified-funding venue is display only — never ranked for trading).
212
+ */
213
+ export function pairExecutability(longExchange, shortExchange) {
214
+ const legs = [...new Set([longExchange, shortExchange])];
215
+ const readOnlyLegs = legs.filter(v => !getVenue(v).roles.includes("trading"));
216
+ const unverifiedLegs = legs.filter(v => !getVenue(v).fundingVerified);
217
+ return { executable: readOnlyLegs.length === 0 && unverifiedLegs.length === 0, readOnlyLegs, unverifiedLegs };
218
+ }
161
219
  function bestMarkPrice(rates) {
162
220
  const rank = (id) => MARK_PRICE_PREFERENCE.indexOf(id);
163
221
  return [...rates].sort((a, b) => rank(a.exchange) - rank(b.exchange))[0].markPrice;
@@ -183,14 +241,28 @@ export async function fetchAllFundingRates(opts) {
183
241
  }
184
242
  const comparisons = [];
185
243
  const minSpread = opts?.minSpread ?? 0;
244
+ let belowMinSpread = 0;
186
245
  for (const [symbol, rates] of rateMap) {
187
246
  // Need at least 2 exchanges to compare
188
247
  if (rates.length < 2)
189
248
  continue;
190
249
  // Sort by hourly rate (ascending)
191
250
  rates.sort((a, b) => a.hourlyRate - b.hourlyRate);
192
- const lowest = rates[0];
193
- const highest = rates[rates.length - 1];
251
+ // The long / short legs and the spread come from verified rows only: an unverified rate is
252
+ // display data (--include-unverified), never ranked. < 2 verified rows → a display-only row.
253
+ const ranked = rates.filter(r => r.fundingVerified);
254
+ if (ranked.length < 2) {
255
+ const unverifiedLegs = [...new Set(rates.filter(r => !r.fundingVerified).map(r => r.exchange))];
256
+ comparisons.push({
257
+ symbol, rates, maxSpreadAnnual: null, longExchange: null, shortExchange: null,
258
+ bestMarkPrice: bestMarkPrice(rates), estHourlyIncomeUsd: null,
259
+ executable: false, readOnlyLegs: [], unverifiedLegs,
260
+ mixedKinds: new Set(rates.map(r => r.rateKind)).size > 1,
261
+ });
262
+ continue;
263
+ }
264
+ const lowest = ranked[0];
265
+ const highest = ranked[ranked.length - 1];
194
266
  // The median filter alone lets a 2-venue group through at up to 2 × MAX_PRICE_DEVIATION_PCT
195
267
  // apart (each side deviates half the gap): the CHOSEN pair must itself agree on price.
196
268
  const pairGapPct = Math.abs(lowest.markPrice - highest.markPrice) / ((lowest.markPrice + highest.markPrice) / 2) * 100;
@@ -203,8 +275,10 @@ export async function fetchAllFundingRates(opts) {
203
275
  continue;
204
276
  }
205
277
  const maxSpreadAnnual = annualizeHourlyRate(highest.hourlyRate - lowest.hourlyRate);
206
- if (maxSpreadAnnual < minSpread)
278
+ if (maxSpreadAnnual < minSpread) {
279
+ belowMinSpread++;
207
280
  continue;
281
+ }
208
282
  // Estimate hourly income for $1000 notional arb
209
283
  const notional = 1000;
210
284
  const longIncome = estimateHourlyFundingFromHourly(lowest.hourlyRate, notional, "long");
@@ -218,13 +292,17 @@ export async function fetchAllFundingRates(opts) {
218
292
  shortExchange: highest.exchange, // short where funding is highest
219
293
  bestMarkPrice: bestMarkPrice(rates),
220
294
  estHourlyIncomeUsd,
295
+ ...pairExecutability(lowest.exchange, highest.exchange),
296
+ mixedKinds: new Set(rates.map(r => r.rateKind)).size > 1,
221
297
  });
222
298
  }
223
- // Sort by spread descending
224
- comparisons.sort((a, b) => b.maxSpreadAnnual - a.maxSpreadAnnual);
299
+ // Sort by spread descending; display-only rows (no ranked pair) last
300
+ comparisons.sort((a, b) => (b.maxSpreadAnnual ?? -Infinity) - (a.maxSpreadAnnual ?? -Infinity));
225
301
  return {
226
302
  timestamp: new Date().toISOString(),
227
303
  symbols: comparisons,
304
+ minSpread,
305
+ belowMinSpread,
228
306
  exchangeStatus,
229
307
  exchangeErrors,
230
308
  };
@@ -233,13 +311,15 @@ export async function fetchAllFundingRates(opts) {
233
311
  * Fetch rates for a single (canonical) symbol across all exchanges.
234
312
  */
235
313
  export async function fetchSymbolFundingRates(symbol, includeUnverified = false) {
236
- const snapshot = await fetchAllFundingRates({ symbols: [symbol.toUpperCase()], includeUnverified });
314
+ // The comparison groups by canonical key: a venue ticker / denomination alias (kPEPE) is
315
+ // looked up under its canonical symbol (1000PEPE), never as-is.
316
+ const snapshot = await fetchAllFundingRates({ symbols: [canonicalQuerySymbol(symbol)], includeUnverified });
237
317
  return snapshot.symbols[0] ?? null;
238
318
  }
239
319
  /**
240
320
  * Fetch spot+perp funding rate spreads.
241
321
  * Since spot has 0 funding cost, the spread is simply |perp funding rate|.
242
- * Only includes symbols available on at least one bot spot venue (def.spot, §12.4 #3).
322
+ * Only includes symbols available on at least one bot spot venue (def.spot).
243
323
  *
244
324
  * Key safety checks:
245
325
  * - U-token mapping: UBTC→BTC, UETH→ETH, USOL→SOL, UFART→FARTCOIN
@@ -247,8 +327,8 @@ export async function fetchSymbolFundingRates(symbol, includeUnverified = false)
247
327
  * (filters out same-ticker-different-token issues like HIP-1 TRUMP ≠ perp TRUMP)
248
328
  */
249
329
  export async function fetchSpotPerpSpreads(opts) {
250
- // Spot legs come from the venue definitions (def.spot, §12): only bot/arb spot venues
251
- // (separate/runtime account + transfer, §12.4 #3). Perp legs: those venues + pacifica.
330
+ // Spot legs come from the venue definitions (def.spot): only bot/arb spot venues
331
+ // (separate/runtime account + transfer). Perp legs: those venues + pacifica.
252
332
  ensureVenuesRegistered();
253
333
  const net = opts?.network ?? sharedApiNetwork();
254
334
  const { listBotSpotVenues, requireSpotAdapter, defaultSpotQuote } = await import("../exchanges/spot-factory.js");