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
@@ -5,12 +5,12 @@ import { tradingV2Of } from "../exchanges/legacy-trading-v2.js";
5
5
  import { invalidateCache } from "../cache.js";
6
6
  import { isDryRun, networkOf, supportsDryRun } from "../cli-flags.js";
7
7
  import chalk from "chalk";
8
- import { formatUsd, printJson, jsonOk, jsonError, makeTable, logSettledRejections, symbolMatch } from "../utils.js";
8
+ import { formatUsd, printJson, jsonOk, jsonError, makeTable, symbolMatch } from "../utils.js";
9
9
  import { computeExecutableSize } from "../liquidity.js";
10
10
  import { computeMatchedSize, computeSpotPerpMatchedSize } from "../arb/index.js";
11
- import { toHourlyRate } from "../funding.js";
11
+ import { getFundingHours, toHourlyRate } from "../funding.js";
12
+ import { fetchVenuesRates } from "../funding/rates.js";
12
13
  import { toHourlyRateByBasis } from "../funding/normalize.js";
13
- import { fetchPacificaPricesRaw, parsePacificaRaw, fetchHyperliquidMetaRaw, parseHyperliquidMetaRaw, fetchLighterOrderBookDetailsRaw, fetchLighterFundingRatesRaw, parseLighterRaw, } from "../shared-api.js";
14
14
  import { fetchAllDexAssetsDetailed, findDexArbPairs, scanDexArb } from "../dex-asset-map.js";
15
15
  import { logExecution, readExecutionLog } from "../execution-log.js";
16
16
  import { smartOrder } from "../smart-order.js";
@@ -18,9 +18,23 @@ import { aggressiveSettleBoost, estimateFundingUntilSettlement, computeBasisRisk
18
18
  import { checkChainMargins, marginEntryBlocks, computeAutoSize, } from "../cross-chain-margin.js";
19
19
  import { loadArbState, addPosition as persistAddPosition, removePosition as persistRemovePosition, updatePosition as persistUpdatePosition, createInitialState, createDaemonStateStore, } from "../arb-state.js";
20
20
  import { handleRates, handleBasisScan, handleDexScan, handleCompare, handleFundingHistory, handleFundingPositions, fetchAllPrices, printGapTable, printTrackSummary, formatGapPrice, } from "./arb.js";
21
+ /** The venue symbol a snapshot / position leg trades on `exchange` (the canonical key when the venue uses it). */
22
+ export function legSymbol(row, exchange) {
23
+ return row.venueSymbols?.[exchange] ?? row.symbol;
24
+ }
25
+ /** longSymbol / shortSymbol of a new position — only when a leg's venue symbol differs from the key. */
26
+ function legSymbolsOf(snap, longExchange, shortExchange) {
27
+ const l = legSymbol(snap, longExchange), sh = legSymbol(snap, shortExchange);
28
+ return l === snap.symbol && sh === snap.symbol ? {} : { longSymbol: l, shortSymbol: sh };
29
+ }
30
+ /** --symbols matches a row by its canonical key or any venue's own symbol (kPEPE or 1000PEPE). */
31
+ export function matchesSymbolFilter(row, filter) {
32
+ const want = new Set(filter.map(s => s.toUpperCase()));
33
+ return want.has(row.symbol.toUpperCase()) || Object.values(row.venueSymbols ?? {}).some(v => want.has(v.toUpperCase()));
34
+ }
21
35
  // ── Fee-Adjusted Net Spread Calculation ──
22
36
  import { getTakerFee } from "../constants.js";
23
- import { assertFundingArbVenue, getVenue, listVenuesWithRole, networkConfig, resolveVenueId, tryResolveVenueId } from "../exchanges/venue.js";
37
+ import { assertFundingArbVenue, getVenue, listVenuesWithRole, resolveVenueId, roleUnsupported, tryResolveVenueId } from "../exchanges/venue.js";
24
38
  import { sharedApiNetwork } from "../api/public/urls.js";
25
39
  import { findSpotQuoteBalance, readSpotQuoteAvailableFresh, requireBotSpotVenue, requireSpotAdapter, spotNetworkOf, spotPairSymbol, spotSharesPerpMargin, sweepSpotQuoteToPerp, transferQuoteToPerp, transferQuoteToSpot } from "../exchanges/spot-factory.js";
26
40
  import { fillSizeString, readSpotBalancesFresh, sizeDecimalsOf, spotLotStepOf, spotMarketFill } from "../exchanges/spot-result.js";
@@ -380,67 +394,60 @@ export function dexCloseBlockedBy(pos, failedDexes) {
380
394
  return null;
381
395
  }
382
396
  const _warnedArbPriceMismatch = new Set();
383
- async function fetchFundingSpreads() {
384
- // Selected network (review L7): the Pacifica / HL / Lighter raw fetchers follow -n via
385
- // setSharedApiNetwork; Aster's REST base comes from the venue definition for that network.
386
- const asterRest = networkConfig(getVenue("aster"), sharedApiNetwork()).restUrl;
387
- // SSOT rule #2: each DEX fetch is independent. allSettled lets us keep the
388
- // multi-DEX comparison alive when one DEX errors out, while the rejection
389
- // reasons get logged to stderr (explicit failure, never silent).
390
- const settled = await Promise.allSettled([
391
- fetchPacificaPricesRaw(),
392
- fetchHyperliquidMetaRaw(),
393
- fetchLighterOrderBookDetailsRaw(),
394
- fetchLighterFundingRatesRaw(),
395
- fetch(`${asterRest}/fapi/v1/premiumIndex`).then(r => r.json()),
396
- fetch(`${asterRest}/fapi/v3/fundingInfo`).then(r => r.json()),
397
- ]);
398
- logSettledRejections(settled, ["pacifica", "hyperliquid", "lighter:orderbook", "lighter:funding", "aster:premiumIndex", "aster:fundingInfo"], "arb-auto");
399
- const pacRes = settled[0].status === "fulfilled" ? settled[0].value : null;
400
- const hlRes = settled[1].status === "fulfilled" ? settled[1].value : null;
401
- const ltDetailsRes = settled[2].status === "fulfilled" ? settled[2].value : null;
402
- const ltFundingRes = settled[3].status === "fulfilled" ? settled[3].value : null;
403
- // Both Aster calls are needed: a rate without its per-symbol interval cannot be normalized.
404
- const astPremiums = settled[4].status === "fulfilled" && settled[5].status === "fulfilled"
405
- ? settled[4].value
406
- : [];
407
- const astFundingInfo = settled[5].status === "fulfilled" ? settled[5].value : [];
397
+ /** " (lastSettled)"-style tag of a snapshot leg's rate kind; "" when the venue is not a leg. */
398
+ const kindTag = (s, ex) => { const k = s.rateKinds?.[ex]; return k === undefined ? "" : `(${k ?? "?"})`; };
399
+ export async function fetchFundingSpreads() {
400
+ // The SAME rate rows as `arb scan --rates` (fetchVenuesRates over each venue's public directory):
401
+ // one rate per venue and canonical symbol (unclassified / ambiguous tickers and non-matchable
402
+ // listings have no canonical and never pair), each with its basis and rateKind. A venue that
403
+ // fails is reported, never silently treated as "no spread".
404
+ const venues = ["pacifica", "hyperliquid", "lighter", "aster"].map(id => getVenue(id));
405
+ const { rates: fetched, exchangeErrors } = await fetchVenuesRates(venues, sharedApiNetwork(), false);
406
+ // Never a leg on an unverified rate (row label; the venue filter above already drops them).
407
+ const rates = fetched.filter(r => r.fundingVerified);
408
+ for (const [ex, msg] of Object.entries(exchangeErrors))
409
+ process.stderr.write(`[arb-auto] ${ex}: funding rates unavailable — ${msg}\n`);
410
+ const pacRates = new Map(), pacPrices = new Map();
411
+ const hlRates = new Map(), hlPrices = new Map();
412
+ const ltRates = new Map(), ltPrices = new Map();
413
+ const astRates = new Map(), astPrices = new Map();
408
414
  const astHoursBySym = new Map();
409
- for (const row of Array.isArray(astFundingInfo) ? astFundingInfo : []) {
410
- const rawSym = String(row.symbol ?? "");
411
- const hours = Number(row.fundingIntervalHours);
412
- if (rawSym.endsWith("USDT") && Number.isFinite(hours) && hours > 0)
413
- astHoursBySym.set(rawSym.replace(/USDT$/, ""), hours);
414
- }
415
- const { rates: pacRates, prices: pacPrices } = parsePacificaRaw(pacRes);
416
- const { rates: hlRates, prices: hlPrices } = parseHyperliquidMetaRaw(hlRes);
417
- const { rates: ltRates, prices: ltPrices } = parseLighterRaw(ltDetailsRes, ltFundingRes);
418
- // Aster: parse premiumIndex.
419
- // SSOT rule #2: never silently coerce missing markPrice/lastFundingRate to 0
420
- // — a 0 markPrice corrupts every downstream sizing calculation, and a 0
421
- // funding rate flips the symbol off the arb radar without explanation.
422
- // Skip the symbol entirely if either field is missing or non-finite.
423
- const astRates = new Map();
424
- const astPrices = new Map();
415
+ const kindsBySym = new Map();
416
+ const venueSymbolsBySym = new Map();
425
417
  const astNoInterval = [];
426
- for (const p of astPremiums ?? []) {
427
- const rawSym = String(p.symbol ?? "");
428
- if (!rawSym.endsWith("USDT"))
429
- continue;
430
- const sym = rawSym.replace(/USDT$/, "");
431
- const fundingRate = Number(p.lastFundingRate);
432
- const markPrice = Number(p.markPrice);
433
- if (!Number.isFinite(fundingRate) || !Number.isFinite(markPrice) || markPrice <= 0) {
434
- // Aster premiumIndex omitted or returned non-numeric values for this
435
- // symbol; skip rather than poison downstream math with a 0.
418
+ for (const r of rates) {
419
+ const sym = r.canonical;
420
+ const kinds = kindsBySym.get(sym) ?? {};
421
+ kinds[r.exchange] = r.rateKind;
422
+ kindsBySym.set(sym, kinds);
423
+ const vs = venueSymbolsBySym.get(sym) ?? {};
424
+ vs[r.exchange] = r.symbol; // the venue's own symbol (orders use it)
425
+ venueSymbolsBySym.set(sym, vs);
426
+ if (r.exchange === "aster") {
427
+ if (r.basis.kind !== "perHours") {
428
+ astNoInterval.push(r.symbol);
429
+ continue;
430
+ }
431
+ astRates.set(sym, r.fundingRate);
432
+ astPrices.set(sym, r.markPrice);
433
+ astHoursBySym.set(sym, r.basis.hours);
436
434
  continue;
437
435
  }
438
- if (!astHoursBySym.has(sym)) {
439
- astNoInterval.push(sym);
440
- continue;
436
+ // The snapshot keeps each venue's rate in the venue's own basis (snapshotHourly /
437
+ // computeAnnualSpread normalize with it): convert from the row's hourly rate.
438
+ const inVenueBasis = r.hourlyRate * getFundingHours(r.exchange);
439
+ if (r.exchange === "pacifica") {
440
+ pacRates.set(sym, inVenueBasis);
441
+ pacPrices.set(sym, r.markPrice);
442
+ }
443
+ else if (r.exchange === "hyperliquid") {
444
+ hlRates.set(sym, inVenueBasis);
445
+ hlPrices.set(sym, r.markPrice);
446
+ }
447
+ else if (r.exchange === "lighter") {
448
+ ltRates.set(sym, inVenueBasis);
449
+ ltPrices.set(sym, r.markPrice);
441
450
  }
442
- astRates.set(sym, fundingRate);
443
- astPrices.set(sym, markPrice);
444
451
  }
445
452
  if (astNoInterval.length > 0) {
446
453
  process.stderr.write(`[arb-auto] aster: ${astNoInterval.length} symbol(s) excluded — no fundingIntervalHours in /fapi/v3/fundingInfo: ${astNoInterval.slice(0, 10).join(", ")}${astNoInterval.length > 10 ? ", …" : ""}\n`);
@@ -528,12 +535,54 @@ async function fetchFundingSpreads() {
528
535
  ltMarkPrice: ltPrices.get(sym) ?? 0,
529
536
  astMarkPrice: astPrices.get(sym) ?? 0,
530
537
  astHours: ast !== undefined ? astHours : null,
538
+ rateKinds: Object.fromEntries(available.map(r => [r.exchange, kindsBySym.get(sym)?.[r.exchange] ?? null])),
539
+ venueSymbols: venueSymbolsBySym.get(sym) ?? {},
531
540
  });
532
541
  }
533
542
  return snapshots.sort((a, b) => Math.abs(b.spread) - Math.abs(a.spread));
534
543
  }
535
544
  // Module-level holder for spot-close handler -- assigned inside registerArbAutoCommands.
536
545
  export let handleSpotClose;
546
+ /**
547
+ * `arb auto` flag validation: every numeric flag must be a finite number in its range, `--size` a
548
+ * number or "auto", `--settle-strategy` one of block | aggressive | off. INVALID_PARAMS otherwise.
549
+ */
550
+ export function assertArbAutoFlags(opts) {
551
+ const num = (flag, key, min, opt = {}) => {
552
+ const raw = opts[key];
553
+ if (raw === undefined)
554
+ return;
555
+ const text = String(raw).trim();
556
+ const v = Number(text);
557
+ const ok = text !== "" && Number.isFinite(v) && (opt.exclusiveMin ? v > min : v >= min) && (!opt.integer || Number.isInteger(v));
558
+ if (!ok) {
559
+ throw new PerpError("INVALID_PARAMS", `${flag} must be ${opt.integer ? "a whole number" : "a number"} ${opt.exclusiveMin ? ">" : ">="} ${min}, got "${text}"`, {
560
+ remediation: `Pass e.g. ${flag} ${opt.exclusiveMin ? min + 1 : Math.max(min, 1)}`,
561
+ });
562
+ }
563
+ };
564
+ num("--min-spread", "minSpread", 0);
565
+ num("--close-spread", "closeSpread", 0);
566
+ if (opts.size !== undefined && String(opts.size).toLowerCase() !== "auto")
567
+ num("--size", "size", 0, { exclusiveMin: true });
568
+ num("--min-size", "minSize", 0);
569
+ num("--max-positions", "maxPositions", 1, { integer: true });
570
+ num("--interval", "interval", 0, { integer: true });
571
+ num("--hold-days", "holdDays", 0, { exclusiveMin: true });
572
+ num("--bridge-cost", "bridgeCost", 0);
573
+ num("--min-margin", "minMargin", 0);
574
+ num("--max-basis", "maxBasis", 0, { exclusiveMin: true });
575
+ num("--cooldown", "cooldown", 0);
576
+ num("--min-oi", "minOi", 0);
577
+ const strat = opts.settleStrategy;
578
+ if (strat !== undefined && !["block", "aggressive", "off"].includes(String(strat))) {
579
+ throw new PerpError("INVALID_PARAMS", `--settle-strategy must be block, aggressive or off, got "${String(strat)}"`, { remediation: "Pass --settle-strategy block (default)" });
580
+ }
581
+ const grade = opts.minGrade;
582
+ if (grade !== undefined && !["A", "B", "C", "D"].includes(String(grade).toUpperCase())) {
583
+ throw new PerpError("INVALID_PARAMS", `--min-grade must be A, B, C or D, got "${String(grade)}"`, { remediation: "Pass --min-grade C (default)" });
584
+ }
585
+ }
537
586
  export function registerArbAutoCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex) {
538
587
  const arb = program.commands.find(c => c.name() === "arb");
539
588
  if (!arb)
@@ -567,6 +616,9 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
567
616
  .option("--min-oi <usd>", "Min OI (USD) to enter (hip3 mode)", "50000")
568
617
  .option("--min-grade <grade>", "Min viability grade: A, B, C, D (hip3 mode)", "C")
569
618
  .action(async (opts) => {
619
+ // Numeric flags are checked up front (also before --background starts a job): a value that is
620
+ // not a number ("--cooldown abc" → NaN) would otherwise silently disable the check it feeds.
621
+ assertArbAutoFlags(opts);
570
622
  // Route to HIP-3 dex-auto logic if --hip3 flag is set
571
623
  if (opts.hip3) {
572
624
  return handleDexAuto({
@@ -647,6 +699,8 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
647
699
  const marginUnknownNotified = new Set();
648
700
  // Track close times per symbol to prevent close→re-entry loops
649
701
  const closeCooldowns = new Map(); // symbol → close timestamp ms
702
+ // --symbols entries already reported as resolving to another canonical symbol (logged once).
703
+ const aliasNoted = new Set();
650
704
  // Consecutive definite close failures per symbol (quarantined at MAX_PAIR_CLOSE_FAILURES)
651
705
  const closeFailures = new Map();
652
706
  // -- State Persistence: Initialize or recover --
@@ -687,7 +741,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
687
741
  (await getAdapterForExchange(p.longExchange)).getPositions(),
688
742
  (await getAdapterForExchange(p.shortExchange)).getPositions(),
689
743
  ]);
690
- const has = (list, side) => list.some(x => symbolMatch(x.symbol, p.symbol) && x.side === side && Math.abs(Number(x.size)) > 0);
744
+ const has = (list, side) => list.some(x => symbolMatch(x.symbol, (side === "long" ? p.longSymbol : p.shortSymbol) ?? p.symbol) && x.side === side && Math.abs(Number(x.size)) > 0);
691
745
  if (has(longs, "long") && has(shorts, "short")) {
692
746
  staleSymbols.delete(p.symbol);
693
747
  return true;
@@ -722,6 +776,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
722
776
  entryMarkPrice: persisted.entryLongPrice,
723
777
  accumulatedFundingUsd: persisted.accumulatedFunding,
724
778
  lastCheckTime: new Date(persisted.lastCheckTime).getTime(),
779
+ longSymbol: persisted.longSymbol, shortSymbol: persisted.shortSymbol,
725
780
  });
726
781
  }
727
782
  daemonState.lastStartTime = new Date().toISOString();
@@ -793,6 +848,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
793
848
  entryMarkPrice: persisted.entryLongPrice,
794
849
  accumulatedFundingUsd: persisted.accumulatedFunding,
795
850
  lastCheckTime: new Date(persisted.lastCheckTime).getTime(),
851
+ longSymbol: persisted.longSymbol, shortSymbol: persisted.shortSymbol,
796
852
  });
797
853
  }
798
854
  }
@@ -813,7 +869,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
813
869
  try {
814
870
  const spreads = await fetchFundingSpreads();
815
871
  const filtered = filterSymbols
816
- ? spreads.filter(s => filterSymbols.includes(s.symbol))
872
+ ? spreads.filter(s => matchesSymbolFilter(s, filterSymbols))
817
873
  : spreads;
818
874
  const now = new Date().toLocaleTimeString();
819
875
  // Check for close conditions on open positions
@@ -821,13 +877,15 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
821
877
  if (stop.requested())
822
878
  break; // stopping: no new close sequence
823
879
  const pos = openPositions[i];
824
- // Quarantined (design §12.3): hands off until a human clears it.
880
+ // Quarantined: hands off until a human clears it.
825
881
  if (isQuarantined(pos.symbol, (m) => console.log(chalk.red(m))))
826
882
  continue;
827
883
  // Per-position isolation: one position's failing exit check (e.g. an
828
884
  // unknown funding basis in snapshotHourly) must not skip the others.
829
885
  try {
830
- const current = filtered.find(s => s.symbol === pos.symbol);
886
+ // By canonical key; a position opened before 0.15.1 is keyed by its (shared) venue symbol.
887
+ const current = filtered.find(s => s.symbol === pos.symbol
888
+ || (legSymbol(s, pos.longExchange) === pos.symbol && legSymbol(s, pos.shortExchange) === pos.symbol));
831
889
  if (!current)
832
890
  continue;
833
891
  const currentSpread = Math.abs(current.spread);
@@ -877,15 +935,15 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
877
935
  try {
878
936
  const longAdapter = await getAdapterForExchange(pos.longExchange);
879
937
  const shortAdapter = await getAdapterForExchange(pos.shortExchange);
880
- // Sequential, reduce-only, verified (design §12.3): long first, then
938
+ // Sequential, reduce-only, verified: long first, then
881
939
  // short. A definite long-close failure changes nothing → retried next
882
940
  // cycle, quarantined after MAX_PAIR_CLOSE_FAILURES in a row. An unknown
883
941
  // outcome, a partial close or an unhedged leg quarantines (no retry).
884
942
  const sizeStr = String(pos.size);
885
943
  const closeRes = await closePerpPairVerified({
886
944
  source: "arb-auto", sym: pos.symbol,
887
- longAdapter, longExch: pos.longExchange, longSym: pos.symbol, longSize: sizeStr,
888
- shortAdapter, shortExch: pos.shortExchange, shortSym: pos.symbol, shortSize: sizeStr, smart: false,
945
+ longAdapter, longExch: pos.longExchange, longSym: pos.longSymbol ?? pos.symbol, longSize: sizeStr,
946
+ shortAdapter, shortExch: pos.shortExchange, shortSym: pos.shortSymbol ?? pos.symbol, shortSize: sizeStr, smart: false,
889
947
  log: (m) => console.log(chalk.red(m)),
890
948
  });
891
949
  if (closeRes.status === "closed") {
@@ -1098,8 +1156,15 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1098
1156
  }
1099
1157
  };
1100
1158
  for (const sym of filterSymbols ?? []) {
1101
- if (!filtered.some(x => x.symbol === sym))
1159
+ // A --symbols entry may be a venue form (kPEPE): it monitors the canonical row it resolves to.
1160
+ const hit = filtered.find(x => matchesSymbolFilter(x, [sym]));
1161
+ if (hit === undefined)
1102
1162
  console.log(chalk.gray(` ${now} SKIP ${sym}: not in this cycle's funding scan (no rate on two compared venues)`));
1163
+ else if (hit.symbol !== sym && !aliasNoted.has(sym)) {
1164
+ aliasNoted.add(sym);
1165
+ const forms = Object.entries(hit.venueSymbols ?? {}).map(([v, vs]) => `${v} ${vs}`).join(" / ");
1166
+ console.log(chalk.gray(` ${now} MONITOR ${sym} → ${hit.symbol} (canonical${forms !== "" ? `; ${forms}` : ""})`));
1167
+ }
1103
1168
  }
1104
1169
  if (openPositions.length >= maxPositions) {
1105
1170
  console.log(chalk.gray(` ${now} NO ENTRY: max positions reached (${openPositions.length}/${maxPositions}: ${openPositions.map(p => `${p.symbol} ${p.longExchange}/${p.shortExchange}`).join(", ")})`));
@@ -1114,7 +1179,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1114
1179
  skip(snap.symbol, "already open");
1115
1180
  continue;
1116
1181
  }
1117
- // Global quarantine (design §12.3): unknown / unhedged outcome on this symbol somewhere.
1182
+ // Global quarantine: unknown / unhedged outcome on this symbol somewhere.
1118
1183
  if (isQuarantined(snap.symbol, (m) => console.log(chalk.red(m)))) {
1119
1184
  skip(snap.symbol, "quarantined");
1120
1185
  continue;
@@ -1195,7 +1260,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1195
1260
  try {
1196
1261
  const longAdapter = await getAdapterForExchange(longExchange);
1197
1262
  const shortAdapter = await getAdapterForExchange(shortExchange);
1198
- actualSizeUsd = await computeAutoSize(longAdapter, shortAdapter, snap.symbol, 0.3);
1263
+ actualSizeUsd = await computeAutoSize(longAdapter, shortAdapter, legSymbol(snap, longExchange), 0.3, legSymbol(snap, shortExchange));
1199
1264
  if (actualSizeUsd <= 0) {
1200
1265
  console.log(chalk.gray(` ${now} SKIP ${snap.symbol}: auto-size returned $0 (insufficient depth/margin)`));
1201
1266
  continue;
@@ -1234,12 +1299,12 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1234
1299
  try {
1235
1300
  const longAdapter = await getAdapterForExchange(longExchange);
1236
1301
  const shortAdapter = await getAdapterForExchange(shortExchange);
1237
- // Sequential verified legs (design §12.3): long, then short sized to the
1302
+ // Sequential verified legs: long, then short sized to the
1238
1303
  // long's verified fill; reduce-only rollback / trim; unknown → quarantine.
1239
1304
  legsStarted = true;
1240
1305
  const res = await openPerpPairVerified({
1241
1306
  source: "arb-auto", sym: snap.symbol,
1242
- longAdapter, longExch: longExchange, longSym: snap.symbol, shortAdapter, shortExch: shortExchange, shortSym: snap.symbol,
1307
+ longAdapter, longExch: longExchange, longSym: legSymbol(snap, longExchange), shortAdapter, shortExch: shortExchange, shortSym: legSymbol(snap, shortExchange),
1243
1308
  size: sizeInAsset, smart: false, log: (m) => console.log(chalk.red(m)),
1244
1309
  });
1245
1310
  legsStarted = false; // outcome known (opened / not opened / quarantined)
@@ -1268,6 +1333,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1268
1333
  entryShortPrice: snap.markPrice,
1269
1334
  accumulatedFunding: 0,
1270
1335
  lastCheckTime: entryTime,
1336
+ ...legSymbolsOf(snap, longExchange, shortExchange),
1271
1337
  });
1272
1338
  }
1273
1339
  catch (e) {
@@ -1291,6 +1357,9 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1291
1357
  });
1292
1358
  console.error(chalk.red(` ${now} ENTRY FAILED ${snap.symbol}: ${res.reason}${res.rolledBack !== null ? ` — rolled back long ${res.rolledBack}; no open exposure` : " — nothing opened"}`));
1293
1359
  donePending();
1360
+ // Cool down like a close: a leg the venue rejects (unknown symbol, lot, margin)
1361
+ // is not re-sent — with its fees and rollback — every cycle.
1362
+ closeCooldowns.set(snap.symbol, Date.now());
1294
1363
  continue;
1295
1364
  }
1296
1365
  else {
@@ -1341,6 +1410,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1341
1410
  entryMarkPrice: snap.markPrice,
1342
1411
  accumulatedFundingUsd: 0,
1343
1412
  lastCheckTime: Date.now(),
1413
+ ...legSymbolsOf(snap, longExchange, shortExchange),
1344
1414
  });
1345
1415
  // Dry-run: record the simulated entry in the in-memory state only (never the real file).
1346
1416
  if (dryRun) {
@@ -1445,7 +1515,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1445
1515
  .option("--duration <minutes>", "Track duration in minutes (with --track)", "10")
1446
1516
  .option("--beep", "Beep on large gaps (with --gaps)")
1447
1517
  .option("--check-liquidity", "Check orderbook depth in live mode (slower)")
1448
- .option("--include-unverified", "With --rates / --compare: also show venues whose funding rate is unverified (qfex) — display only, never ranked for trading")
1518
+ .option("--include-unverified", "With --rates / --compare: also show venues whose funding rate is unverified (qfex; txflow — unofficial API; variational; grvt — indicative rate; extended) — display only, never ranked for trading")
1449
1519
  .action(async (opts) => {
1450
1520
  // ── Price gap modes ──
1451
1521
  if (opts.gaps || opts.track || opts.alert) {
@@ -1594,7 +1664,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1594
1664
  const topCheck = filtered.slice(0, 5);
1595
1665
  await Promise.allSettled(topCheck.map(async (s) => {
1596
1666
  try {
1597
- const [hlOB, ltOB] = await Promise.all([fetchHLOrderbook(s.symbol), fetchLighterOrderbook(s.symbol)]);
1667
+ const [hlOB, ltOB] = await Promise.all([fetchHLOrderbook(legSymbol(s, "hyperliquid")), fetchLighterOrderbook(legSymbol(s, "lighter"))]);
1598
1668
  const hlDepth = hlOB.reduce((sum, l) => sum + l[0] * l[1], 0);
1599
1669
  const ltDepth = ltOB.reduce((sum, l) => sum + l[0] * l[1], 0);
1600
1670
  const hlBest = hlOB[0]?.[0] ?? 0;
@@ -1613,12 +1683,13 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1613
1683
  const grossColor = grossSpread >= 50 ? chalk.green.bold : grossSpread >= 30 ? chalk.green : chalk.yellow;
1614
1684
  const netColor = netSpread >= 20 ? chalk.green : netSpread >= 0 ? chalk.yellow : chalk.red;
1615
1685
  const rates = [];
1686
+ // Each leg names which rate it is (lastSettled / accruing / predicted; ? = unverified).
1616
1687
  if (s.pacRate)
1617
- rates.push(`PAC:${(s.pacRate * 100).toFixed(4)}%`);
1688
+ rates.push(`PAC:${(s.pacRate * 100).toFixed(4)}%${kindTag(s, "pacifica")}`);
1618
1689
  if (s.hlRate)
1619
- rates.push(`HL:${(s.hlRate * 100).toFixed(4)}%`);
1690
+ rates.push(`HL:${(s.hlRate * 100).toFixed(4)}%${kindTag(s, "hyperliquid")}`);
1620
1691
  if (s.ltRate)
1621
- rates.push(`LT:${(s.ltRate * 100).toFixed(4)}%`);
1692
+ rates.push(`LT:${(s.ltRate * 100).toFixed(4)}%${kindTag(s, "lighter")}`);
1622
1693
  let liqInfo = "";
1623
1694
  const ld = liqData.get(s.symbol);
1624
1695
  if (ld)
@@ -1682,8 +1753,10 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1682
1753
  const netSpread = computeNetSpread(s.annualSpreadPct, holdDays, rtCost, effectiveBridgeCost, sizeUsd);
1683
1754
  return {
1684
1755
  mode: "spot-perp", symbol: s.symbol, perpExchange: s.perpExchange,
1685
- spotExchanges: s.spotExchanges, grossSpreadPct: s.annualSpreadPct,
1686
- netSpreadPct: netSpread, estFeesPct: rtCost,
1756
+ // grossSpread / netSpread: the names every other arb scan row uses (same % values);
1757
+ // the *Pct names stay for existing consumers.
1758
+ spotExchanges: s.spotExchanges, grossSpreadPct: s.annualSpreadPct, grossSpread: s.annualSpreadPct,
1759
+ netSpreadPct: netSpread, netSpread, estFeesPct: rtCost,
1687
1760
  perpFundingRate: s.perpFundingRate, direction: s.direction,
1688
1761
  markPrice: s.bestMarkPrice, estHourlyIncomeUsd: s.estHourlyIncomeUsd,
1689
1762
  breakEvenDays: rtCost > 0 ? Math.ceil(rtCost / (s.annualSpreadPct / 365)) : 0,
@@ -1747,7 +1820,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1747
1820
  const shortPrice = pFor(s.shortExch);
1748
1821
  const mid = (longPrice + shortPrice) / 2;
1749
1822
  const priceGapPct = mid > 0 ? Math.abs(longPrice - shortPrice) / mid * 100 : 0;
1750
- return { mode: "perp-perp", symbol: s.symbol, longExch: s.longExch, shortExch: s.shortExch, markPrice: s.markPrice, grossSpread, netSpread: net, estFeesPct: rtCost, priceGapPct: Math.round(priceGapPct * 10000) / 10000 };
1823
+ return { mode: "perp-perp", symbol: s.symbol, longExch: s.longExch, shortExch: s.shortExch, markPrice: s.markPrice, grossSpread, grossSpreadPct: grossSpread, netSpread: net, netSpreadPct: net, estFeesPct: rtCost, priceGapPct: Math.round(priceGapPct * 10000) / 10000 };
1751
1824
  });
1752
1825
  // Merge spot-perp results if "all" mode
1753
1826
  const spotPerp = (opts.mode === "all" && spotSpreads) ? spotSpreads.map(s => {
@@ -1757,8 +1830,8 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1757
1830
  const net = computeNetSpread(s.annualSpreadPct, holdDays, rtCost, effectiveBridgeCost, sizeUsd);
1758
1831
  return {
1759
1832
  mode: "spot-perp", symbol: s.symbol, perpExchange: s.perpExchange,
1760
- spotExchanges: s.spotExchanges, grossSpread: s.annualSpreadPct,
1761
- netSpread: net, estFeesPct: rtCost,
1833
+ spotExchanges: s.spotExchanges, grossSpread: s.annualSpreadPct, grossSpreadPct: s.annualSpreadPct,
1834
+ netSpread: net, netSpreadPct: net, estFeesPct: rtCost,
1762
1835
  perpFundingRate: s.perpFundingRate, direction: s.direction,
1763
1836
  markPrice: s.bestMarkPrice, breakEvenDays: rtCost > 0 ? Math.ceil(rtCost / (s.annualSpreadPct / 365)) : 0,
1764
1837
  };
@@ -1787,14 +1860,15 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1787
1860
  const priceGapPct = mid > 0 ? Math.abs(longPrice - shortPrice) / mid * 100 : 0;
1788
1861
  const gapColor = priceGapPct >= 0.5 ? chalk.red : priceGapPct >= 0.1 ? chalk.yellow : chalk.gray;
1789
1862
  const rates = [];
1863
+ // Each leg names which rate it is (lastSettled / accruing / predicted; ? = unverified).
1790
1864
  if (s.pacRate)
1791
- rates.push(`PAC:${(s.pacRate * 100).toFixed(4)}%`);
1865
+ rates.push(`PAC:${(s.pacRate * 100).toFixed(4)}%${kindTag(s, "pacifica")}`);
1792
1866
  if (s.hlRate)
1793
- rates.push(`HL:${(s.hlRate * 100).toFixed(4)}%`);
1867
+ rates.push(`HL:${(s.hlRate * 100).toFixed(4)}%${kindTag(s, "hyperliquid")}`);
1794
1868
  if (s.ltRate)
1795
- rates.push(`LT:${(s.ltRate * 100).toFixed(4)}%`);
1869
+ rates.push(`LT:${(s.ltRate * 100).toFixed(4)}%${kindTag(s, "lighter")}`);
1796
1870
  if (s.astRate)
1797
- rates.push(`AST:${(s.astRate * 100).toFixed(4)}%`);
1871
+ rates.push(`AST:${(s.astRate * 100).toFixed(4)}%${kindTag(s, "aster")}`);
1798
1872
  return [
1799
1873
  chalk.white.bold(s.symbol),
1800
1874
  grossColor(`${grossSpread.toFixed(1)}%`),
@@ -1857,6 +1931,8 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1857
1931
  for (const ex of [longExch, shortExch]) {
1858
1932
  const venue = getVenue(ex);
1859
1933
  if (!venue.roles.includes("trading")) {
1934
+ if (venue.gatedRoles !== null)
1935
+ throw roleUnsupported(venue, "trading");
1860
1936
  throw unsupported(venue.displayName, "trading", "no trading API — it cannot be an arb leg", `Use a trading venue: ${listVenuesWithRole("trading").map(v => v.id).join(", ")}`);
1861
1937
  }
1862
1938
  }
@@ -1867,7 +1943,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1867
1943
  console.log(chalk.red(" Long and short exchange must be different."));
1868
1944
  return;
1869
1945
  }
1870
- // Global quarantine (design §12.3): an unknown / unhedged outcome on this symbol
1946
+ // Global quarantine: an unknown / unhedged outcome on this symbol
1871
1947
  // anywhere blocks new trades until a human clears it.
1872
1948
  const quarantined = quarantineEntryFor(sym, (m) => console.error(m));
1873
1949
  if (quarantined !== null) {
@@ -1875,7 +1951,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1875
1951
  remediation: `${quarantined.remediation} — then: perp strategy quarantine-clear ${sym}`,
1876
1952
  });
1877
1953
  }
1878
- // A venue whose funding rate is unverified is never a funding-arb leg (design §14.12 #6).
1954
+ // A venue whose funding rate is unverified is never a funding-arb leg.
1879
1955
  assertFundingArbVenue(longExch);
1880
1956
  assertFundingArbVenue(shortExch);
1881
1957
  const longAdapter = await getAdapterForExchange(longExch);
@@ -1991,7 +2067,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1991
2067
  }
1992
2068
  if (!isJson())
1993
2069
  console.log(chalk.gray(` Executing legs sequentially (long, then short sized to the verified long fill)...\n`));
1994
- // 5. Sequential verified legs (design §12.3). The second leg is sized to the
2070
+ // 5. Sequential verified legs. The second leg is sized to the
1995
2071
  // first leg's VERIFIED fill, any shortfall is trimmed off the long
1996
2072
  // (reduce-only, verified — always, --reconcile is implied), a definite short
1997
2073
  // failure rolls the long back reduce-only, and an unknown outcome (incl. a
@@ -2070,12 +2146,14 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
2070
2146
  // Resolve exchange aliases
2071
2147
  spotExch = resolveVenueId(spotExch);
2072
2148
  perpExch = resolveVenueId(perpExch);
2073
- // Validate spot exchange: bot/arb spot scope (§12.4 #3) — throws CAPABILITY_UNSUPPORTED
2149
+ // Validate spot exchange: bot/arb spot scope — throws CAPABILITY_UNSUPPORTED
2074
2150
  // for venues without spot or outside the scope (pacifica shared margin, arcus swaps).
2075
2151
  const spotDef = requireBotSpotVenue(spotExch.replace(/-spot$/, ""));
2076
2152
  spotExch = spotDef.id;
2077
2153
  perpExch = tryResolveVenueId(perpExch) ?? perpExch;
2078
2154
  if (!getVenue(perpExch).roles.includes("trading")) {
2155
+ if (getVenue(perpExch).gatedRoles !== null)
2156
+ throw roleUnsupported(getVenue(perpExch), "trading");
2079
2157
  throw unsupported(perpExch, "trading", "no trading API — it cannot be the perp leg", `Use a trading venue: ${listVenuesWithRole("trading").map(v => v.id).join(", ")}`);
2080
2158
  }
2081
2159
  assertFundingArbVenue(perpExch);
@@ -2093,7 +2171,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
2093
2171
  return;
2094
2172
  console.log(chalk.red(` ${msg}`));
2095
2173
  };
2096
- // Global quarantine (design §12.3): checked before anything is read or moved.
2174
+ // Global quarantine: checked before anything is read or moved.
2097
2175
  const quarantined = quarantineEntryFor(sym, (m) => console.error(m));
2098
2176
  if (quarantined !== null) {
2099
2177
  throw new PerpError("RISK_VIOLATION", `${sym} is quarantined (${quarantined.strategy}: ${quarantined.reason})`, {
@@ -2273,7 +2351,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
2273
2351
  // 7. Execute both legs. Same exchange shares a nonce → sequential; else parallel.
2274
2352
  if (!isJson())
2275
2353
  console.log(chalk.gray(` Executing ${sameVenue ? "sequentially" : "both legs simultaneously"}...\n`));
2276
- // The perp leg goes through placePerpVerified (design §12.3): a fill is the measured
2354
+ // The perp leg goes through placePerpVerified: a fill is the measured
2277
2355
  // position delta; an unknown outcome is ORDER_PENDING (quarantine, never roll back).
2278
2356
  let spotResult;
2279
2357
  let perpResult;
@@ -2523,7 +2601,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
2523
2601
  remediation: `Pass both: perp arb close ${sym} --spot-exch <spot venue> --perp-exch <perp venue>`,
2524
2602
  });
2525
2603
  }
2526
- // Load adapters (bot/arb spot scope — §12.4 #3)
2604
+ // Load adapters (bot/arb spot scope)
2527
2605
  const spotDef = requireBotSpotVenue(spotExch.replace(/-spot$/, ""));
2528
2606
  const spotAdapter = await requireSpotAdapter(spotDef.id, await getAdapter(spotDef.id));
2529
2607
  const perpAdapter = await getAdapter(perpExch);
@@ -2551,12 +2629,12 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
2551
2629
  // Read before anything moves — a failure aborts the close.
2552
2630
  const preCloseQuote = await readSpotQuoteAvailableFresh(spotAdapter);
2553
2631
  // Same exchange → sequential (nonce collision), different → parallel.
2554
- // The spot leg's REAL fill decides the outcome (§12.4 #5): only a confirmed spot sale
2632
+ // The spot leg's REAL fill decides the outcome: only a confirmed spot sale
2555
2633
  // counts as closed; legs are re-matched so no naked short / unhedged spot is left.
2556
2634
  const sameVenue = spotDef.id === (tryResolveVenueId(perpExch) ?? perpExch);
2557
2635
  const closeDec = sizeDecimalsOf(closeSize);
2558
- // The perp buy-back and every re-match order go through placePerpVerified (design
2559
- // §12.3): only a measured position change counts; an unknown outcome stops the close.
2636
+ // The perp buy-back and every re-match order go through placePerpVerified:
2637
+ // only a measured position change counts; an unknown outcome stops the close.
2560
2638
  let spotResult;
2561
2639
  let perpResult;
2562
2640
  if (sameVenue) {
@@ -3073,7 +3151,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
3073
3151
  (p.long.dex === pos.shortDex && p.short.dex === pos.longDex)));
3074
3152
  const currentSpread = current?.annualSpread ?? 0;
3075
3153
  if (currentSpread <= closeSpread || !current) {
3076
- // Quarantined (design §12.3): hands off until a human clears it.
3154
+ // Quarantined: hands off until a human clears it.
3077
3155
  if (isQuarantined(pos.underlying, (m) => console.error(chalk.red(m))))
3078
3156
  continue;
3079
3157
  const reason = !current ? "pair disappeared" : `spread ${currentSpread.toFixed(1)}% <= ${closeSpread}%`;
@@ -13,6 +13,14 @@ export interface PriceSnapshot {
13
13
  expensive: string;
14
14
  }
15
15
  export declare function fetchAllPrices(): Promise<PriceSnapshot[]>;
16
+ /**
17
+ * Cross-venue price gaps by venue symbol. The same ticker on two venues is not proof of the same
18
+ * instrument (Pacifica SAMSUNG 165.8 USD vs Lighter SAMSUNG 220,597 — a KRW-quoted listing): each
19
+ * symbol's prices go through the funding scan's median price-outlier guard (dropPriceOutliers,
20
+ * MAX_PRICE_DEVIATION_PCT, one warning per symbol / venue) before a gap is computed, so a gap is
21
+ * only reported between prices that agree within the guard.
22
+ */
23
+ export declare function buildGapSnapshots(pacPrices: Map<string, number>, hlPrices: Map<string, number>, ltPrices: Map<string, number>): PriceSnapshot[];
16
24
  export declare function formatGapPrice(p: number): string;
17
25
  export declare function printGapTable(snapshots: PriceSnapshot[], minGap: number): void;
18
26
  export declare function printTrackSummary(symbol: string, samples: {
@@ -29,6 +37,18 @@ export declare function handleRates(isJson: () => boolean, opts: {
29
37
  top?: string;
30
38
  includeUnverified?: boolean;
31
39
  }): Promise<void>;
40
+ /**
41
+ * Cross-venue price basis — a PRICE comparison, independent of funding.
42
+ *
43
+ * Venue scope: every venue with the "trading" role (both legs must be executable), whether or not
44
+ * its funding rate is verified; market-data-only venues (TxFlow, unofficial API) are out. Prices
45
+ * come from each venue's market directory and are grouped by `canonical` symbol with the funding
46
+ * scan's guards (fetchCanonicalPriceGroups): unclassified / ambiguous tickers have no canonical and
47
+ * never pair, and a venue whose mark deviates more than MAX_PRICE_DEVIATION_PCT from the group
48
+ * median is dropped. The chosen cheap / rich pair must itself agree within MAX_PRICE_DEVIATION_PCT:
49
+ * a wider gap is a different asset under the same ticker (BB on Lighter 8.50 vs Aster 0.0091).
50
+ * A venue that fails is reported on stderr and is absent from the scan, never silently empty.
51
+ */
32
52
  export declare function handleBasisScan(isJson: () => boolean, opts: {
33
53
  minBasis: string;
34
54
  symbol?: string;