perp-cli 0.3.3

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 (325) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -0
  3. package/dist/__tests__/alert-logic.test.d.ts +1 -0
  4. package/dist/__tests__/alert-logic.test.js +107 -0
  5. package/dist/__tests__/arb-auto-3dex.test.d.ts +1 -0
  6. package/dist/__tests__/arb-auto-3dex.test.js +397 -0
  7. package/dist/__tests__/arb-history-stats.test.d.ts +1 -0
  8. package/dist/__tests__/arb-history-stats.test.js +176 -0
  9. package/dist/__tests__/arb-logic.test.d.ts +1 -0
  10. package/dist/__tests__/arb-logic.test.js +84 -0
  11. package/dist/__tests__/arb-manage.test.d.ts +1 -0
  12. package/dist/__tests__/arb-manage.test.js +253 -0
  13. package/dist/__tests__/arb-new-features.test.d.ts +1 -0
  14. package/dist/__tests__/arb-new-features.test.js +457 -0
  15. package/dist/__tests__/arb-sizing.test.d.ts +1 -0
  16. package/dist/__tests__/arb-sizing.test.js +48 -0
  17. package/dist/__tests__/arb-state.test.d.ts +1 -0
  18. package/dist/__tests__/arb-state.test.js +284 -0
  19. package/dist/__tests__/arb-userflow.test.d.ts +1 -0
  20. package/dist/__tests__/arb-userflow.test.js +945 -0
  21. package/dist/__tests__/arb-utils.test.d.ts +1 -0
  22. package/dist/__tests__/arb-utils.test.js +264 -0
  23. package/dist/__tests__/bot-conditions.test.d.ts +1 -0
  24. package/dist/__tests__/bot-conditions.test.js +341 -0
  25. package/dist/__tests__/client-id-tracker.test.d.ts +1 -0
  26. package/dist/__tests__/client-id-tracker.test.js +137 -0
  27. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +1 -0
  28. package/dist/__tests__/commands/new-atomic-commands.test.js +502 -0
  29. package/dist/__tests__/commands/order-intent.test.d.ts +1 -0
  30. package/dist/__tests__/commands/order-intent.test.js +600 -0
  31. package/dist/__tests__/commands/trade-commands.test.d.ts +1 -0
  32. package/dist/__tests__/commands/trade-commands.test.js +821 -0
  33. package/dist/__tests__/config.test.d.ts +1 -0
  34. package/dist/__tests__/config.test.js +86 -0
  35. package/dist/__tests__/cross-chain-margin.test.d.ts +1 -0
  36. package/dist/__tests__/cross-chain-margin.test.js +287 -0
  37. package/dist/__tests__/dex-asset-map.test.d.ts +1 -0
  38. package/dist/__tests__/dex-asset-map.test.js +191 -0
  39. package/dist/__tests__/errors.test.d.ts +1 -0
  40. package/dist/__tests__/errors.test.js +110 -0
  41. package/dist/__tests__/event-stream.test.d.ts +1 -0
  42. package/dist/__tests__/event-stream.test.js +276 -0
  43. package/dist/__tests__/exchanges/interface.test.d.ts +1 -0
  44. package/dist/__tests__/exchanges/interface.test.js +132 -0
  45. package/dist/__tests__/exchanges/mock-adapter.d.ts +69 -0
  46. package/dist/__tests__/exchanges/mock-adapter.js +137 -0
  47. package/dist/__tests__/execution-log.test.d.ts +1 -0
  48. package/dist/__tests__/execution-log.test.js +106 -0
  49. package/dist/__tests__/funding-calc.test.d.ts +1 -0
  50. package/dist/__tests__/funding-calc.test.js +71 -0
  51. package/dist/__tests__/funding-history.test.d.ts +1 -0
  52. package/dist/__tests__/funding-history.test.js +343 -0
  53. package/dist/__tests__/funding-rates.test.d.ts +1 -0
  54. package/dist/__tests__/funding-rates.test.js +342 -0
  55. package/dist/__tests__/funding.test.d.ts +1 -0
  56. package/dist/__tests__/funding.test.js +173 -0
  57. package/dist/__tests__/gap-logic.test.d.ts +1 -0
  58. package/dist/__tests__/gap-logic.test.js +43 -0
  59. package/dist/__tests__/hip3-dex.test.d.ts +1 -0
  60. package/dist/__tests__/hip3-dex.test.js +234 -0
  61. package/dist/__tests__/integration/agent-features.integration.test.d.ts +1 -0
  62. package/dist/__tests__/integration/agent-features.integration.test.js +553 -0
  63. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +13 -0
  64. package/dist/__tests__/integration/atomic-commands.integration.test.js +246 -0
  65. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +1 -0
  66. package/dist/__tests__/integration/bridge-simulation.integration.test.js +453 -0
  67. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +1 -0
  68. package/dist/__tests__/integration/bridge-strict.integration.test.js +812 -0
  69. package/dist/__tests__/integration/bridge.integration.test.d.ts +1 -0
  70. package/dist/__tests__/integration/bridge.integration.test.js +309 -0
  71. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +1 -0
  72. package/dist/__tests__/integration/cli-e2e.integration.test.js +202 -0
  73. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +1 -0
  74. package/dist/__tests__/integration/dex-arb.integration.test.js +116 -0
  75. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +13 -0
  76. package/dist/__tests__/integration/envelope-consistency.integration.test.js +205 -0
  77. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +1 -0
  78. package/dist/__tests__/integration/hip3-dex.integration.test.js +147 -0
  79. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +1 -0
  80. package/dist/__tests__/integration/hyperliquid.integration.test.js +79 -0
  81. package/dist/__tests__/integration/lighter.integration.test.d.ts +1 -0
  82. package/dist/__tests__/integration/lighter.integration.test.js +53 -0
  83. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +9 -0
  84. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +236 -0
  85. package/dist/__tests__/integration/order-verification.integration.test.d.ts +1 -0
  86. package/dist/__tests__/integration/order-verification.integration.test.js +321 -0
  87. package/dist/__tests__/integration/pacifica.integration.test.d.ts +1 -0
  88. package/dist/__tests__/integration/pacifica.integration.test.js +75 -0
  89. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +1 -0
  90. package/dist/__tests__/integration/response-shapes.integration.test.js +278 -0
  91. package/dist/__tests__/liquidity.test.d.ts +1 -0
  92. package/dist/__tests__/liquidity.test.js +225 -0
  93. package/dist/__tests__/plan-executor.test.d.ts +1 -0
  94. package/dist/__tests__/plan-executor.test.js +314 -0
  95. package/dist/__tests__/position-history.test.d.ts +1 -0
  96. package/dist/__tests__/position-history.test.js +367 -0
  97. package/dist/__tests__/retry.test.d.ts +1 -0
  98. package/dist/__tests__/retry.test.js +310 -0
  99. package/dist/__tests__/risk-assessment.test.d.ts +1 -0
  100. package/dist/__tests__/risk-assessment.test.js +145 -0
  101. package/dist/__tests__/security-adversarial.test.d.ts +1 -0
  102. package/dist/__tests__/security-adversarial.test.js +574 -0
  103. package/dist/__tests__/strategies.test.d.ts +1 -0
  104. package/dist/__tests__/strategies.test.js +539 -0
  105. package/dist/__tests__/trade-execution.test.d.ts +1 -0
  106. package/dist/__tests__/trade-execution.test.js +129 -0
  107. package/dist/__tests__/trade-validator.test.d.ts +1 -0
  108. package/dist/__tests__/trade-validator.test.js +655 -0
  109. package/dist/__tests__/utils.test.d.ts +1 -0
  110. package/dist/__tests__/utils.test.js +76 -0
  111. package/dist/api/public/hyperliquid.d.ts +18 -0
  112. package/dist/api/public/hyperliquid.js +82 -0
  113. package/dist/api/public/index.d.ts +8 -0
  114. package/dist/api/public/index.js +8 -0
  115. package/dist/api/public/lighter.d.ts +24 -0
  116. package/dist/api/public/lighter.js +100 -0
  117. package/dist/api/public/pacifica.d.ts +17 -0
  118. package/dist/api/public/pacifica.js +54 -0
  119. package/dist/api/public/urls.d.ts +12 -0
  120. package/dist/api/public/urls.js +33 -0
  121. package/dist/arb/history-stats.d.ts +44 -0
  122. package/dist/arb/history-stats.js +135 -0
  123. package/dist/arb/index.d.ts +4 -0
  124. package/dist/arb/index.js +4 -0
  125. package/dist/arb/sizing.d.ts +23 -0
  126. package/dist/arb/sizing.js +96 -0
  127. package/dist/arb/state.d.ts +51 -0
  128. package/dist/arb/state.js +112 -0
  129. package/dist/arb/utils.d.ts +81 -0
  130. package/dist/arb/utils.js +267 -0
  131. package/dist/arb-history-stats.d.ts +5 -0
  132. package/dist/arb-history-stats.js +5 -0
  133. package/dist/arb-sizing.d.ts +5 -0
  134. package/dist/arb-sizing.js +5 -0
  135. package/dist/arb-state.d.ts +5 -0
  136. package/dist/arb-state.js +5 -0
  137. package/dist/arb-utils.d.ts +5 -0
  138. package/dist/arb-utils.js +5 -0
  139. package/dist/bot/conditions.d.ts +32 -0
  140. package/dist/bot/conditions.js +141 -0
  141. package/dist/bot/config.d.ts +76 -0
  142. package/dist/bot/config.js +160 -0
  143. package/dist/bot/engine.d.ts +8 -0
  144. package/dist/bot/engine.js +519 -0
  145. package/dist/bot/presets.d.ts +11 -0
  146. package/dist/bot/presets.js +296 -0
  147. package/dist/bridge-engine.d.ts +133 -0
  148. package/dist/bridge-engine.js +1487 -0
  149. package/dist/cache.d.ts +25 -0
  150. package/dist/cache.js +99 -0
  151. package/dist/cli-spec.d.ts +50 -0
  152. package/dist/cli-spec.js +75 -0
  153. package/dist/client-id-tracker.d.ts +25 -0
  154. package/dist/client-id-tracker.js +76 -0
  155. package/dist/commands/account.d.ts +3 -0
  156. package/dist/commands/account.js +425 -0
  157. package/dist/commands/agent.d.ts +3 -0
  158. package/dist/commands/agent.js +386 -0
  159. package/dist/commands/alert.d.ts +2 -0
  160. package/dist/commands/alert.js +421 -0
  161. package/dist/commands/analytics.d.ts +3 -0
  162. package/dist/commands/analytics.js +311 -0
  163. package/dist/commands/arb/index.d.ts +3 -0
  164. package/dist/commands/arb/index.js +921 -0
  165. package/dist/commands/arb-auto.d.ts +54 -0
  166. package/dist/commands/arb-auto.js +1328 -0
  167. package/dist/commands/arb-manage.d.ts +5 -0
  168. package/dist/commands/arb-manage.js +5 -0
  169. package/dist/commands/arb.d.ts +2 -0
  170. package/dist/commands/arb.js +347 -0
  171. package/dist/commands/backtest.d.ts +2 -0
  172. package/dist/commands/backtest.js +327 -0
  173. package/dist/commands/bot.d.ts +3 -0
  174. package/dist/commands/bot.js +412 -0
  175. package/dist/commands/bridge.d.ts +2 -0
  176. package/dist/commands/bridge.js +396 -0
  177. package/dist/commands/dashboard.d.ts +3 -0
  178. package/dist/commands/dashboard.js +176 -0
  179. package/dist/commands/deposit.d.ts +4 -0
  180. package/dist/commands/deposit.js +573 -0
  181. package/dist/commands/dex.d.ts +3 -0
  182. package/dist/commands/dex.js +114 -0
  183. package/dist/commands/env.d.ts +2 -0
  184. package/dist/commands/env.js +136 -0
  185. package/dist/commands/funding.d.ts +2 -0
  186. package/dist/commands/funding.js +347 -0
  187. package/dist/commands/gap.d.ts +2 -0
  188. package/dist/commands/gap.js +305 -0
  189. package/dist/commands/health.d.ts +2 -0
  190. package/dist/commands/health.js +67 -0
  191. package/dist/commands/history.d.ts +2 -0
  192. package/dist/commands/history.js +235 -0
  193. package/dist/commands/init.d.ts +15 -0
  194. package/dist/commands/init.js +266 -0
  195. package/dist/commands/jobs.d.ts +2 -0
  196. package/dist/commands/jobs.js +133 -0
  197. package/dist/commands/manage.d.ts +4 -0
  198. package/dist/commands/manage.js +309 -0
  199. package/dist/commands/market.d.ts +3 -0
  200. package/dist/commands/market.js +225 -0
  201. package/dist/commands/plan.d.ts +3 -0
  202. package/dist/commands/plan.js +95 -0
  203. package/dist/commands/portfolio.d.ts +3 -0
  204. package/dist/commands/portfolio.js +169 -0
  205. package/dist/commands/rebalance.d.ts +3 -0
  206. package/dist/commands/rebalance.js +293 -0
  207. package/dist/commands/risk.d.ts +3 -0
  208. package/dist/commands/risk.js +169 -0
  209. package/dist/commands/run.d.ts +3 -0
  210. package/dist/commands/run.js +202 -0
  211. package/dist/commands/settings.d.ts +2 -0
  212. package/dist/commands/settings.js +102 -0
  213. package/dist/commands/stream.d.ts +5 -0
  214. package/dist/commands/stream.js +123 -0
  215. package/dist/commands/trade.d.ts +3 -0
  216. package/dist/commands/trade.js +1273 -0
  217. package/dist/commands/wallet.d.ts +14 -0
  218. package/dist/commands/wallet.js +602 -0
  219. package/dist/commands/withdraw.d.ts +3 -0
  220. package/dist/commands/withdraw.js +187 -0
  221. package/dist/config.d.ts +5 -0
  222. package/dist/config.js +68 -0
  223. package/dist/cross-chain-margin.d.ts +46 -0
  224. package/dist/cross-chain-margin.js +107 -0
  225. package/dist/dashboard/server.d.ts +80 -0
  226. package/dist/dashboard/server.js +340 -0
  227. package/dist/dashboard/ui.d.ts +4 -0
  228. package/dist/dashboard/ui.js +538 -0
  229. package/dist/dashboard/ws-feeds.d.ts +29 -0
  230. package/dist/dashboard/ws-feeds.js +660 -0
  231. package/dist/dex-asset-map.d.ts +80 -0
  232. package/dist/dex-asset-map.js +201 -0
  233. package/dist/errors.d.ts +109 -0
  234. package/dist/errors.js +84 -0
  235. package/dist/event-stream.d.ts +25 -0
  236. package/dist/event-stream.js +168 -0
  237. package/dist/exchanges/hyperliquid.d.ts +212 -0
  238. package/dist/exchanges/hyperliquid.js +931 -0
  239. package/dist/exchanges/interface.d.ts +95 -0
  240. package/dist/exchanges/interface.js +5 -0
  241. package/dist/exchanges/lighter.d.ts +159 -0
  242. package/dist/exchanges/lighter.js +793 -0
  243. package/dist/exchanges/pacifica.d.ts +51 -0
  244. package/dist/exchanges/pacifica.js +248 -0
  245. package/dist/execution-log.d.ts +36 -0
  246. package/dist/execution-log.js +102 -0
  247. package/dist/funding/history.d.ts +63 -0
  248. package/dist/funding/history.js +266 -0
  249. package/dist/funding/index.d.ts +3 -0
  250. package/dist/funding/index.js +3 -0
  251. package/dist/funding/normalize.d.ts +39 -0
  252. package/dist/funding/normalize.js +66 -0
  253. package/dist/funding/rates.d.ts +45 -0
  254. package/dist/funding/rates.js +172 -0
  255. package/dist/funding-history.d.ts +5 -0
  256. package/dist/funding-history.js +5 -0
  257. package/dist/funding-rates.d.ts +5 -0
  258. package/dist/funding-rates.js +5 -0
  259. package/dist/funding.d.ts +5 -0
  260. package/dist/funding.js +5 -0
  261. package/dist/index.d.ts +2 -0
  262. package/dist/index.js +458 -0
  263. package/dist/jobs.d.ts +37 -0
  264. package/dist/jobs.js +152 -0
  265. package/dist/liquidity.d.ts +34 -0
  266. package/dist/liquidity.js +100 -0
  267. package/dist/mcp-server.d.ts +9 -0
  268. package/dist/mcp-server.js +1206 -0
  269. package/dist/pacifica/client.d.ts +111 -0
  270. package/dist/pacifica/client.js +310 -0
  271. package/dist/pacifica/constants.d.ts +27 -0
  272. package/dist/pacifica/constants.js +47 -0
  273. package/dist/pacifica/deposit.d.ts +14 -0
  274. package/dist/pacifica/deposit.js +78 -0
  275. package/dist/pacifica/index.d.ts +6 -0
  276. package/dist/pacifica/index.js +11 -0
  277. package/dist/pacifica/signing.d.ts +49 -0
  278. package/dist/pacifica/signing.js +97 -0
  279. package/dist/pacifica/types/account.d.ts +42 -0
  280. package/dist/pacifica/types/account.js +1 -0
  281. package/dist/pacifica/types/index.d.ts +6 -0
  282. package/dist/pacifica/types/index.js +6 -0
  283. package/dist/pacifica/types/lake.d.ts +18 -0
  284. package/dist/pacifica/types/lake.js +1 -0
  285. package/dist/pacifica/types/market.d.ts +64 -0
  286. package/dist/pacifica/types/market.js +1 -0
  287. package/dist/pacifica/types/order.d.ts +92 -0
  288. package/dist/pacifica/types/order.js +1 -0
  289. package/dist/pacifica/types/position.d.ts +25 -0
  290. package/dist/pacifica/types/position.js +1 -0
  291. package/dist/pacifica/types/ws.d.ts +34 -0
  292. package/dist/pacifica/types/ws.js +41 -0
  293. package/dist/pacifica/ws-client.d.ts +42 -0
  294. package/dist/pacifica/ws-client.js +180 -0
  295. package/dist/plan-executor.d.ts +48 -0
  296. package/dist/plan-executor.js +280 -0
  297. package/dist/position-history.d.ts +68 -0
  298. package/dist/position-history.js +222 -0
  299. package/dist/rebalance.d.ts +64 -0
  300. package/dist/rebalance.js +142 -0
  301. package/dist/retry.d.ts +74 -0
  302. package/dist/retry.js +129 -0
  303. package/dist/risk.d.ts +48 -0
  304. package/dist/risk.js +156 -0
  305. package/dist/settings.d.ts +19 -0
  306. package/dist/settings.js +45 -0
  307. package/dist/shared-api.d.ts +5 -0
  308. package/dist/shared-api.js +5 -0
  309. package/dist/strategies/dca.d.ts +25 -0
  310. package/dist/strategies/dca.js +114 -0
  311. package/dist/strategies/funding-arb.d.ts +15 -0
  312. package/dist/strategies/funding-arb.js +281 -0
  313. package/dist/strategies/grid.d.ts +34 -0
  314. package/dist/strategies/grid.js +185 -0
  315. package/dist/strategies/trailing-stop.d.ts +17 -0
  316. package/dist/strategies/trailing-stop.js +121 -0
  317. package/dist/strategies/twap.d.ts +20 -0
  318. package/dist/strategies/twap.js +78 -0
  319. package/dist/trade-validator.d.ts +39 -0
  320. package/dist/trade-validator.js +154 -0
  321. package/dist/utils.d.ts +38 -0
  322. package/dist/utils.js +110 -0
  323. package/package.json +63 -0
  324. package/skills/perp-cli/SKILL.md +149 -0
  325. package/skills/perp-cli/references/commands.md +143 -0
@@ -0,0 +1,278 @@
1
+ import { describe, it, expect, beforeAll } from "vitest";
2
+ import { HyperliquidAdapter } from "../../exchanges/hyperliquid.js";
3
+ import { fetchAllDexAssets, findDexArbPairs, scanDexArb, } from "../../dex-asset-map.js";
4
+ import { startEventStream } from "../../event-stream.js";
5
+ /**
6
+ * Integration tests: validate that ALL API response shapes match what
7
+ * user-facing code expects.
8
+ *
9
+ * Hits real Hyperliquid mainnet API (read-only, no private key needed).
10
+ * Catches shape mismatches between live API data and our TypeScript interfaces.
11
+ *
12
+ * Run:
13
+ * pnpm --filter perp-cli test -- --testPathPattern response-shapes.integration
14
+ */
15
+ // Dummy private key for read-only operations (never signs anything)
16
+ const DUMMY_KEY = "0x" + "1".repeat(64);
17
+ /**
18
+ * Find a market by base symbol, handling possible suffixes (e.g., "BTC-PERP").
19
+ * The SDK may return symbols with or without a -PERP suffix depending on version.
20
+ */
21
+ function findMarket(markets, base) {
22
+ return (markets.find((m) => m.symbol === base) ??
23
+ markets.find((m) => m.symbol === `${base}-PERP`) ??
24
+ markets.find((m) => m.symbol.toUpperCase().startsWith(base.toUpperCase())));
25
+ }
26
+ describe("Response Shape Validation (Hyperliquid Mainnet)", () => {
27
+ let adapter;
28
+ beforeAll(async () => {
29
+ adapter = new HyperliquidAdapter(DUMMY_KEY, false);
30
+ await adapter.init();
31
+ }, 30000);
32
+ // ── 1. ExchangeMarketInfo shape ──────────────────────────────────────
33
+ describe("1. ExchangeMarketInfo shape", () => {
34
+ it("every market has all required fields with correct types", async () => {
35
+ const markets = await adapter.getMarkets();
36
+ expect(markets.length).toBeGreaterThanOrEqual(10);
37
+ // BTC and ETH must be present (may have -PERP suffix)
38
+ const btc = findMarket(markets, "BTC");
39
+ const eth = findMarket(markets, "ETH");
40
+ expect(btc).toBeTruthy();
41
+ expect(eth).toBeTruthy();
42
+ for (const m of markets) {
43
+ // symbol: non-empty string
44
+ expect(typeof m.symbol).toBe("string");
45
+ expect(m.symbol.length).toBeGreaterThan(0);
46
+ // markPrice: string, parseable as number > 0
47
+ expect(typeof m.markPrice).toBe("string");
48
+ const mark = Number(m.markPrice);
49
+ expect(Number.isNaN(mark)).toBe(false);
50
+ expect(mark).toBeGreaterThan(0);
51
+ // indexPrice: string
52
+ expect(typeof m.indexPrice).toBe("string");
53
+ // fundingRate: string, parseable as number
54
+ expect(typeof m.fundingRate).toBe("string");
55
+ const funding = Number(m.fundingRate);
56
+ expect(Number.isNaN(funding)).toBe(false);
57
+ // volume24h: string
58
+ expect(typeof m.volume24h).toBe("string");
59
+ // openInterest: string
60
+ expect(typeof m.openInterest).toBe("string");
61
+ // maxLeverage: number > 0
62
+ expect(typeof m.maxLeverage).toBe("number");
63
+ expect(m.maxLeverage).toBeGreaterThan(0);
64
+ }
65
+ }, 30000);
66
+ });
67
+ // ── 2. Orderbook shape ───────────────────────────────────────────────
68
+ describe("2. ExchangeOrder shape (getOrderbook)", () => {
69
+ it("BTC orderbook has correct bid/ask tuple structure", async () => {
70
+ const book = await adapter.getOrderbook("BTC");
71
+ // bids and asks are arrays
72
+ expect(Array.isArray(book.bids)).toBe(true);
73
+ expect(Array.isArray(book.asks)).toBe(true);
74
+ // Both have entries
75
+ expect(book.bids.length).toBeGreaterThan(0);
76
+ expect(book.asks.length).toBeGreaterThan(0);
77
+ // Each entry is a [price, size] tuple of strings
78
+ for (const [price, size] of book.bids) {
79
+ expect(typeof price).toBe("string");
80
+ expect(typeof size).toBe("string");
81
+ expect(Number.isNaN(Number(price))).toBe(false);
82
+ expect(Number.isNaN(Number(size))).toBe(false);
83
+ }
84
+ for (const [price, size] of book.asks) {
85
+ expect(typeof price).toBe("string");
86
+ expect(typeof size).toBe("string");
87
+ expect(Number.isNaN(Number(price))).toBe(false);
88
+ expect(Number.isNaN(Number(size))).toBe(false);
89
+ }
90
+ // Spread is positive: best bid < best ask
91
+ const bestBid = Number(book.bids[0][0]);
92
+ const bestAsk = Number(book.asks[0][0]);
93
+ expect(bestBid).toBeLessThan(bestAsk);
94
+ }, 30000);
95
+ });
96
+ // ── 3. HIP-3 Deployed Dexes shape ───────────────────────────────────
97
+ describe("3. HIP-3 Deployed Dexes shape", () => {
98
+ it("listDeployedDexes returns correctly shaped dex entries", async () => {
99
+ const dexes = await adapter.listDeployedDexes();
100
+ expect(Array.isArray(dexes)).toBe(true);
101
+ expect(dexes.length).toBeGreaterThanOrEqual(3);
102
+ for (const dex of dexes) {
103
+ // name: non-empty string
104
+ expect(typeof dex.name).toBe("string");
105
+ expect(dex.name.length).toBeGreaterThan(0);
106
+ // assets: string array with length > 0
107
+ expect(Array.isArray(dex.assets)).toBe(true);
108
+ expect(dex.assets.length).toBeGreaterThan(0);
109
+ for (const asset of dex.assets) {
110
+ expect(typeof asset).toBe("string");
111
+ }
112
+ }
113
+ // Known dex "xyz" is present
114
+ const xyz = dexes.find((d) => d.name === "xyz");
115
+ expect(xyz).toBeTruthy();
116
+ // Asset names from deployed dexes contain ":" prefix (e.g., "xyz:TSLA")
117
+ for (const dex of dexes) {
118
+ for (const asset of dex.assets) {
119
+ expect(asset).toContain(":");
120
+ }
121
+ }
122
+ }, 30000);
123
+ });
124
+ // ── 4. DexAsset shape from fetchAllDexAssets ─────────────────────────
125
+ describe("4. DexAsset shape from fetchAllDexAssets", () => {
126
+ let allAssets;
127
+ beforeAll(async () => {
128
+ allAssets = await fetchAllDexAssets();
129
+ }, 30000);
130
+ it("every asset has all required fields with correct types", () => {
131
+ expect(allAssets.length).toBeGreaterThan(200);
132
+ for (const asset of allAssets) {
133
+ // raw: non-empty string
134
+ expect(typeof asset.raw).toBe("string");
135
+ expect(asset.raw.length).toBeGreaterThan(0);
136
+ // base: non-empty string
137
+ expect(typeof asset.base).toBe("string");
138
+ expect(asset.base.length).toBeGreaterThan(0);
139
+ // dex: non-empty string
140
+ expect(typeof asset.dex).toBe("string");
141
+ expect(asset.dex.length).toBeGreaterThan(0);
142
+ // markPrice: number > 0
143
+ expect(typeof asset.markPrice).toBe("number");
144
+ expect(asset.markPrice).toBeGreaterThan(0);
145
+ // fundingRate: number (not NaN)
146
+ expect(typeof asset.fundingRate).toBe("number");
147
+ expect(Number.isNaN(asset.fundingRate)).toBe(false);
148
+ // maxLeverage: number
149
+ expect(typeof asset.maxLeverage).toBe("number");
150
+ // openInterest: number
151
+ expect(typeof asset.openInterest).toBe("number");
152
+ // volume24h: number
153
+ expect(typeof asset.volume24h).toBe("number");
154
+ // szDecimals: number >= 0
155
+ expect(typeof asset.szDecimals).toBe("number");
156
+ expect(asset.szDecimals).toBeGreaterThanOrEqual(0);
157
+ }
158
+ });
159
+ it("assets come from at least 4 different dexes", () => {
160
+ const dexes = new Set(allAssets.map((a) => a.dex));
161
+ expect(dexes.size).toBeGreaterThanOrEqual(4);
162
+ });
163
+ });
164
+ // ── 5. DexArbPair shape from findDexArbPairs ────────────────────────
165
+ describe("5. DexArbPair shape from findDexArbPairs", () => {
166
+ it("TSLA pairs across dexes have correct shape and constraints", async () => {
167
+ const allAssets = await fetchAllDexAssets();
168
+ const tslaAssets = allAssets.filter((a) => a.base === "TSLA");
169
+ // TSLA should exist on multiple dexes
170
+ expect(tslaAssets.length).toBeGreaterThanOrEqual(2);
171
+ const pairs = findDexArbPairs(tslaAssets);
172
+ expect(pairs.length).toBeGreaterThanOrEqual(1);
173
+ for (const pair of pairs) {
174
+ // underlying: string
175
+ expect(typeof pair.underlying).toBe("string");
176
+ expect(pair.underlying).toBe("TSLA");
177
+ // long and short: DexAsset objects
178
+ expect(pair.long).toBeTruthy();
179
+ expect(pair.short).toBeTruthy();
180
+ expect(typeof pair.long.dex).toBe("string");
181
+ expect(typeof pair.short.dex).toBe("string");
182
+ // long.dex !== short.dex
183
+ expect(pair.long.dex).not.toBe(pair.short.dex);
184
+ // priceGapPct < 5 (same underlying, prices should be close)
185
+ expect(pair.priceGapPct).toBeLessThan(5);
186
+ // annualSpread: reasonable number (not NaN, not Infinity)
187
+ expect(typeof pair.annualSpread).toBe("number");
188
+ expect(Number.isNaN(pair.annualSpread)).toBe(false);
189
+ expect(Number.isFinite(pair.annualSpread)).toBe(true);
190
+ }
191
+ }, 30000);
192
+ });
193
+ // ── 6. scanDexArb full pipeline ──────────────────────────────────────
194
+ describe("6. scanDexArb full pipeline", () => {
195
+ it("returns sorted array with valid pairs", async () => {
196
+ const pairs = await scanDexArb({ minAnnualSpread: 5 });
197
+ expect(Array.isArray(pairs)).toBe(true);
198
+ // Sorted by annualSpread descending
199
+ for (let i = 1; i < pairs.length; i++) {
200
+ expect(pairs[i].annualSpread).toBeLessThanOrEqual(pairs[i - 1].annualSpread);
201
+ }
202
+ // Every pair has all required fields and no same-dex pairs
203
+ for (const pair of pairs) {
204
+ expect(typeof pair.underlying).toBe("string");
205
+ expect(pair.underlying.length).toBeGreaterThan(0);
206
+ expect(pair.long).toBeTruthy();
207
+ expect(pair.short).toBeTruthy();
208
+ expect(pair.long.dex).not.toBe(pair.short.dex);
209
+ expect(typeof pair.annualSpread).toBe("number");
210
+ expect(pair.annualSpread).toBeGreaterThanOrEqual(5);
211
+ expect(Number.isFinite(pair.annualSpread)).toBe(true);
212
+ expect(typeof pair.priceGapPct).toBe("number");
213
+ expect(Number.isFinite(pair.priceGapPct)).toBe(true);
214
+ }
215
+ }, 30000);
216
+ });
217
+ // ── 7. Event stream shape (single poll cycle) ───────────────────────
218
+ describe("7. Event stream shape (single poll cycle)", () => {
219
+ it("emits correctly shaped events without crashing", async () => {
220
+ const events = [];
221
+ const controller = new AbortController();
222
+ // Run a single poll cycle then abort
223
+ const streamPromise = startEventStream(adapter, {
224
+ intervalMs: 100_000, // large interval so we only get one poll
225
+ onEvent: (event) => {
226
+ events.push(event);
227
+ },
228
+ signal: controller.signal,
229
+ });
230
+ // Wait briefly for the initial poll to complete, then abort
231
+ await new Promise((resolve) => setTimeout(resolve, 5000));
232
+ controller.abort();
233
+ // Wait for the stream to finish
234
+ await streamPromise;
235
+ // With a dummy key, positions/orders may be empty but the poll should
236
+ // complete without crashing. Any emitted events must conform to shape.
237
+ for (const event of events) {
238
+ // type: valid EventType string
239
+ expect(typeof event.type).toBe("string");
240
+ expect(event.type.length).toBeGreaterThan(0);
241
+ // exchange: string
242
+ expect(typeof event.exchange).toBe("string");
243
+ expect(event.exchange).toBe("hyperliquid");
244
+ // timestamp: ISO format string
245
+ expect(typeof event.timestamp).toBe("string");
246
+ expect(new Date(event.timestamp).toISOString()).toBe(event.timestamp);
247
+ // data: object
248
+ expect(typeof event.data).toBe("object");
249
+ expect(event.data).not.toBeNull();
250
+ }
251
+ }, 30000);
252
+ });
253
+ // ── 8. Funding rate / price sanity checks ────────────────────────────
254
+ describe("8. Funding rate and price sanity checks", () => {
255
+ it("BTC and ETH have reasonable funding rates and prices", async () => {
256
+ const markets = await adapter.getMarkets();
257
+ // Handle possible -PERP suffix from SDK
258
+ const btc = findMarket(markets, "BTC");
259
+ const eth = findMarket(markets, "ETH");
260
+ expect(btc).toBeTruthy();
261
+ expect(eth).toBeTruthy();
262
+ // BTC funding rate between -0.01 and 0.01 (1% per period is extreme)
263
+ const btcFunding = Number(btc.fundingRate);
264
+ expect(btcFunding).toBeGreaterThan(-0.01);
265
+ expect(btcFunding).toBeLessThan(0.01);
266
+ // ETH funding rate between -0.01 and 0.01
267
+ const ethFunding = Number(eth.fundingRate);
268
+ expect(ethFunding).toBeGreaterThan(-0.01);
269
+ expect(ethFunding).toBeLessThan(0.01);
270
+ // BTC mark price > $100 (catches off-by-order-of-magnitude parsing bugs)
271
+ const btcPrice = Number(btc.markPrice);
272
+ expect(btcPrice).toBeGreaterThan(100);
273
+ // ETH mark price > $10
274
+ const ethPrice = Number(eth.markPrice);
275
+ expect(ethPrice).toBeGreaterThan(10);
276
+ }, 30000);
277
+ });
278
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,225 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { computeExecutableSize, checkArbLiquidity } from "../liquidity.js";
3
+ // ──────────────────────────────────────────────
4
+ // computeExecutableSize — orderbook walking
5
+ // ──────────────────────────────────────────────
6
+ describe("computeExecutableSize — empty book", () => {
7
+ it("returns zeroed result for empty levels", () => {
8
+ const result = computeExecutableSize([], 1000);
9
+ expect(result.maxSize).toBe(0);
10
+ expect(result.avgFillPrice).toBe(0);
11
+ expect(result.slippagePct).toBe(0);
12
+ expect(result.depthUsd).toBe(0);
13
+ expect(result.canFillFull).toBe(false);
14
+ expect(result.recommendedSize).toBe(0);
15
+ });
16
+ });
17
+ describe("computeExecutableSize — single level", () => {
18
+ it("fills entirely from one level when sufficient", () => {
19
+ // One level at $100, size 100 = $10,000 USD. Request $500.
20
+ const result = computeExecutableSize([["100", "100"]], 500);
21
+ expect(result.maxSize).toBeCloseTo(5); // $500 / $100
22
+ expect(result.avgFillPrice).toBeCloseTo(100);
23
+ expect(result.slippagePct).toBeCloseTo(0);
24
+ expect(result.canFillFull).toBe(true);
25
+ expect(result.depthUsd).toBeCloseTo(10000);
26
+ });
27
+ it("partially fills when level is insufficient", () => {
28
+ // One level at $100, size 2 = $200 USD. Request $500.
29
+ const result = computeExecutableSize([["100", "2"]], 500);
30
+ expect(result.maxSize).toBeCloseTo(2);
31
+ expect(result.avgFillPrice).toBeCloseTo(100);
32
+ // Can't fill 95% → canFillFull = false
33
+ expect(result.canFillFull).toBe(false);
34
+ expect(result.depthUsd).toBeCloseTo(200);
35
+ });
36
+ });
37
+ describe("computeExecutableSize — multiple levels", () => {
38
+ it("walks through levels to fill requested size", () => {
39
+ // Use prices well within slippage tolerance (default 0.5%)
40
+ // bestPrice = 100, slippageLimit = 100.5
41
+ // All prices must be <= 100.5 to not be stopped by slippage check
42
+ const levels = [
43
+ ["100", "5"], // $500
44
+ ["100.2", "5"], // $501 — well within 0.5% of $100
45
+ ["100.4", "10"], // $1004 — also within 0.5%
46
+ ];
47
+ // Request $1000. Level 1: $500 fully consumed (5 units).
48
+ // Level 2: remaining $500, levelUsd $501 > $500, partial fill: 500/100.2 ≈ 4.99 units.
49
+ const result = computeExecutableSize(levels, 1000);
50
+ expect(result.maxSize).toBeCloseTo(5 + 500 / 100.2, 2);
51
+ expect(result.canFillFull).toBe(true);
52
+ expect(result.avgFillPrice).toBeCloseTo(1000 / result.maxSize, 2);
53
+ expect(result.slippagePct).toBeGreaterThan(0);
54
+ expect(result.slippagePct).toBeLessThan(0.5);
55
+ });
56
+ it("computes slippage as avg fill vs best price", () => {
57
+ // Use wider slippage tolerance (5%) so all levels are included
58
+ const levels = [
59
+ ["1000", "0.1"], // $100
60
+ ["1010", "0.1"], // $101
61
+ ["1020", "10"], // $10,200
62
+ ];
63
+ // With 5% tolerance, slippageLimit = 1000 * 1.05 = 1050
64
+ // All levels are within tolerance.
65
+ // Request $10,000. Level 1: $100 (fully consumed). Level 2: $101 (fully consumed).
66
+ // Level 3: remaining = $9799, levelUsd = $10200 > remaining → partial fill.
67
+ const result = computeExecutableSize(levels, 10000, 5);
68
+ // Most of the fill happens at $1020
69
+ expect(result.avgFillPrice).toBeGreaterThan(1000);
70
+ // slippagePct = abs((avgFillPrice - 1000) / 1000) * 100
71
+ expect(result.slippagePct).toBeGreaterThan(1);
72
+ expect(result.slippagePct).toBeLessThan(3);
73
+ });
74
+ it("stops walking when slippage limit is exceeded", () => {
75
+ const levels = [
76
+ ["100", "1"], // $100
77
+ ["100.4", "1"], // $100.40 — within 0.5%
78
+ ["101", "100"], // $10,100 — 1% above best, exceeds 0.5% default
79
+ ];
80
+ // Request $5000 with default 0.5% slippage.
81
+ // slippageLimit = 100 * (1 + 0.5/100) = 100.5
82
+ // Level 1: $100 (consumed), level 2: $100.4 (consumed), level 3: $101 > $100.5 → stop
83
+ const result = computeExecutableSize(levels, 5000, 0.5);
84
+ // Should only consume levels 1 and 2
85
+ expect(result.maxSize).toBeCloseTo(2);
86
+ expect(result.canFillFull).toBe(false);
87
+ });
88
+ it("respects custom slippage tolerance", () => {
89
+ const levels = [
90
+ ["100", "1"],
91
+ ["102", "100"], // 2% above best
92
+ ];
93
+ // With 3% tolerance, level 2 is within range
94
+ const result = computeExecutableSize(levels, 5000, 3);
95
+ expect(result.canFillFull).toBe(true);
96
+ expect(result.maxSize).toBeGreaterThan(1);
97
+ });
98
+ it("considers canFillFull at 95% threshold", () => {
99
+ // Need $1000. If we fill $950, canFillFull = true (95%)
100
+ const levels = [
101
+ ["100", "9.5"], // $950
102
+ ];
103
+ const result = computeExecutableSize(levels, 1000);
104
+ expect(result.canFillFull).toBe(true);
105
+ // Need $1000. If we fill $940, canFillFull = false (94%)
106
+ const levels2 = [
107
+ ["100", "9.4"], // $940
108
+ ];
109
+ const result2 = computeExecutableSize(levels2, 1000);
110
+ expect(result2.canFillFull).toBe(false);
111
+ });
112
+ });
113
+ describe("computeExecutableSize — depth calculation", () => {
114
+ it("accumulates total depth across all iterated levels", () => {
115
+ const levels = [
116
+ ["100", "10"], // $1000
117
+ ["101", "10"], // $1010
118
+ ["102", "10"], // $1020
119
+ ];
120
+ // Request $500. Level 1 has $1000 > $500, so partial fill on level 1.
121
+ // But the loop: totalDepthUsd is accumulated BEFORE checking remainingUsd.
122
+ // So level 1: totalDepthUsd += $1000, then fills $500 partial, then remainingUsd = 0.
123
+ // Level 2: totalDepthUsd += $1010 (accumulated before the break on remainingUsd <= 0).
124
+ // Actually: after level 1 fill, filledNotional = $500, so remainingUsd = 0 → breaks at start of level 2.
125
+ // Wait — let's trace: for level 1, totalDepthUsd += 1000, price 100 not > slippageLimit,
126
+ // remaining = 500, levelUsd = 1000 > 500 → partial fill. filledNotional = 500.
127
+ // Next iteration level 2: totalDepthUsd += 1010, then remaining = 500 - 500 = 0 → break.
128
+ // So totalDepthUsd = 1000 + 1010 = 2010.
129
+ const result = computeExecutableSize(levels, 500);
130
+ expect(result.depthUsd).toBeCloseTo(2010);
131
+ // But only $500 was actually filled from level 1
132
+ expect(result.maxSize).toBeCloseTo(5); // 500 / 100
133
+ expect(result.canFillFull).toBe(true);
134
+ });
135
+ });
136
+ // ──────────────────────────────────────────────
137
+ // checkArbLiquidity — cross-exchange check
138
+ // ──────────────────────────────────────────────
139
+ function mockAdapter(name, asks, bids) {
140
+ return {
141
+ name,
142
+ getOrderbook: vi.fn().mockResolvedValue({ asks, bids }),
143
+ getMarkets: vi.fn().mockResolvedValue([]),
144
+ getBalance: vi.fn().mockResolvedValue({ equity: "1000", available: "800", marginUsed: "200", unrealizedPnl: "0" }),
145
+ getPositions: vi.fn().mockResolvedValue([]),
146
+ getOpenOrders: vi.fn().mockResolvedValue([]),
147
+ getOrderHistory: vi.fn().mockResolvedValue([]),
148
+ getTradeHistory: vi.fn().mockResolvedValue([]),
149
+ getRecentTrades: vi.fn().mockResolvedValue([]),
150
+ getFundingHistory: vi.fn().mockResolvedValue([]),
151
+ getFundingPayments: vi.fn().mockResolvedValue([]),
152
+ getKlines: vi.fn().mockResolvedValue([]),
153
+ marketOrder: vi.fn().mockResolvedValue({}),
154
+ limitOrder: vi.fn().mockResolvedValue({}),
155
+ editOrder: vi.fn().mockResolvedValue({}),
156
+ cancelOrder: vi.fn().mockResolvedValue({}),
157
+ cancelAllOrders: vi.fn().mockResolvedValue({}),
158
+ setLeverage: vi.fn().mockResolvedValue({}),
159
+ stopOrder: vi.fn().mockResolvedValue({}),
160
+ };
161
+ }
162
+ describe("checkArbLiquidity", () => {
163
+ it("returns viable when both sides have sufficient liquidity", async () => {
164
+ const longAdapter = mockAdapter("exchange-a", [["60000", "10"], ["60010", "10"]], // asks
165
+ [["59990", "10"]]);
166
+ const shortAdapter = mockAdapter("exchange-b", [["60005", "10"]], // asks
167
+ [["59995", "10"], ["59985", "10"]]);
168
+ const result = await checkArbLiquidity(longAdapter, shortAdapter, "BTC", 1000);
169
+ expect(result.viable).toBe(true);
170
+ expect(result.adjustedSizeUsd).toBeGreaterThan(0);
171
+ expect(result.adjustedSizeUsd).toBeLessThanOrEqual(1000);
172
+ });
173
+ it("returns not viable when liquidity is too thin (less than 20% of requested)", async () => {
174
+ const longAdapter = mockAdapter("exchange-a", [["60000", "0.001"]], // asks: ~$60
175
+ [["59990", "1"]]);
176
+ const shortAdapter = mockAdapter("exchange-b", [["60005", "1"]], [["59995", "0.001"]]);
177
+ // Request $10000. Both sides have ~$60 → way below 20% = $2000 threshold
178
+ const result = await checkArbLiquidity(longAdapter, shortAdapter, "BTC", 10000);
179
+ expect(result.viable).toBe(false);
180
+ expect(result.adjustedSizeUsd).toBe(0);
181
+ });
182
+ it("returns not viable when cross-exchange price gap exceeds 2%", async () => {
183
+ const longAdapter = mockAdapter("exchange-a", [["60000", "100"]], // asks
184
+ [["59900", "100"]]);
185
+ const shortAdapter = mockAdapter("exchange-b", [["62000", "100"]], // asks
186
+ [["61500", "100"]]);
187
+ const result = await checkArbLiquidity(longAdapter, shortAdapter, "BTC", 1000);
188
+ expect(result.viable).toBe(false);
189
+ });
190
+ it("adjusts size down when one side has limited liquidity", async () => {
191
+ const longAdapter = mockAdapter("exchange-a", [["60000", "0.05"]], // asks: $3000
192
+ [["59990", "10"]]);
193
+ const shortAdapter = mockAdapter("exchange-b", [["60010", "10"]], [["59995", "10"]]);
194
+ // Request $5000. Long side only has $3000 on asks.
195
+ const result = await checkArbLiquidity(longAdapter, shortAdapter, "BTC", 5000);
196
+ expect(result.viable).toBe(true);
197
+ expect(result.adjustedSizeUsd).toBeLessThan(5000);
198
+ expect(result.adjustedSizeUsd).toBeGreaterThan(0);
199
+ });
200
+ it("returns not viable when orderbook fetch fails", async () => {
201
+ const longAdapter = mockAdapter("exchange-a", [], []);
202
+ longAdapter.getOrderbook = vi.fn().mockRejectedValue(new Error("timeout"));
203
+ const shortAdapter = mockAdapter("exchange-b", [], []);
204
+ const result = await checkArbLiquidity(longAdapter, shortAdapter, "BTC", 1000);
205
+ expect(result.viable).toBe(false);
206
+ expect(result.adjustedSizeUsd).toBe(0);
207
+ });
208
+ it("invokes log callback for diagnostics", async () => {
209
+ const longAdapter = mockAdapter("exchange-a", [["60000", "0.001"]], // tiny asks
210
+ [["59990", "1"]]);
211
+ const shortAdapter = mockAdapter("exchange-b", [["60005", "1"]], [["59995", "0.001"]]);
212
+ const logFn = vi.fn();
213
+ await checkArbLiquidity(longAdapter, shortAdapter, "BTC", 10000, 0.5, logFn);
214
+ expect(logFn).toHaveBeenCalled();
215
+ expect(logFn.mock.calls[0][0]).toContain("[LIQ]");
216
+ });
217
+ it("caps adjusted size to requested size when liquidity is ample", async () => {
218
+ const longAdapter = mockAdapter("exchange-a", [["60000", "100"]], // $6M asks
219
+ [["59990", "100"]]);
220
+ const shortAdapter = mockAdapter("exchange-b", [["60005", "100"]], [["59995", "100"]]);
221
+ const result = await checkArbLiquidity(longAdapter, shortAdapter, "BTC", 1000);
222
+ expect(result.viable).toBe(true);
223
+ expect(result.adjustedSizeUsd).toBeLessThanOrEqual(1000);
224
+ });
225
+ });
@@ -0,0 +1 @@
1
+ export {};