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 @@
1
+ export {};
@@ -0,0 +1,342 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ /**
3
+ * Tests for the funding-rates module.
4
+ *
5
+ * These test the core comparison/normalization logic using mocked API responses.
6
+ * Integration tests that hit real APIs live in integration/.
7
+ */
8
+ // Mock fetch globally before importing the module
9
+ const mockFetch = vi.fn();
10
+ vi.stubGlobal("fetch", mockFetch);
11
+ // Import after mocking fetch
12
+ const { fetchAllFundingRates, fetchSymbolFundingRates, TOP_SYMBOLS } = await import("../funding-rates.js");
13
+ const { invalidateCache } = await import("../cache.js");
14
+ // ── Helpers to build mock API responses ──
15
+ function makePacificaResponse(rates) {
16
+ return { data: rates };
17
+ }
18
+ function makeHyperliquidResponse(assets, ctxs) {
19
+ return [{ universe: assets }, ctxs];
20
+ }
21
+ function makeLighterResponse(details, fundingRates) {
22
+ return {
23
+ details: { order_book_details: details },
24
+ funding: { funding_rates: fundingRates.map(fr => ({ exchange: "lighter", ...fr })) },
25
+ };
26
+ }
27
+ function setupMockFetch(opts) {
28
+ mockFetch.mockImplementation(async (url, init) => {
29
+ const urlStr = typeof url === "string" ? url : url.toString();
30
+ // Pacifica
31
+ if (urlStr.includes("pacifica.fi")) {
32
+ if (opts.pacError)
33
+ throw new Error("Pacifica API error");
34
+ return {
35
+ json: async () => makePacificaResponse(opts.pac ?? []),
36
+ };
37
+ }
38
+ // Hyperliquid
39
+ if (urlStr.includes("hyperliquid.xyz")) {
40
+ if (opts.hlError)
41
+ throw new Error("HL API error");
42
+ const hl = opts.hl ?? { assets: [], ctxs: [] };
43
+ return {
44
+ json: async () => makeHyperliquidResponse(hl.assets, hl.ctxs),
45
+ };
46
+ }
47
+ // Lighter - two endpoints
48
+ if (urlStr.includes("zklighter") && urlStr.includes("orderBookDetails")) {
49
+ if (opts.ltError)
50
+ throw new Error("Lighter API error");
51
+ const lt = opts.lt ?? { details: [], funding: [] };
52
+ return {
53
+ json: async () => ({ order_book_details: lt.details }),
54
+ };
55
+ }
56
+ if (urlStr.includes("zklighter") && urlStr.includes("funding-rates")) {
57
+ if (opts.ltError)
58
+ throw new Error("Lighter API error");
59
+ const lt = opts.lt ?? { details: [], funding: [] };
60
+ return {
61
+ json: async () => ({ funding_rates: lt.funding.map(fr => ({ exchange: "lighter", ...fr })) }),
62
+ };
63
+ }
64
+ throw new Error(`Unexpected fetch: ${urlStr}`);
65
+ });
66
+ }
67
+ beforeEach(() => {
68
+ mockFetch.mockReset();
69
+ invalidateCache();
70
+ });
71
+ // ──────────────────────────────────────────────
72
+ // TOP_SYMBOLS
73
+ // ──────────────────────────────────────────────
74
+ describe("TOP_SYMBOLS", () => {
75
+ it("includes major crypto assets", () => {
76
+ expect(TOP_SYMBOLS).toContain("BTC");
77
+ expect(TOP_SYMBOLS).toContain("ETH");
78
+ expect(TOP_SYMBOLS).toContain("SOL");
79
+ });
80
+ it("has at least 10 symbols", () => {
81
+ expect(TOP_SYMBOLS.length).toBeGreaterThanOrEqual(10);
82
+ });
83
+ });
84
+ // ──────────────────────────────────────────────
85
+ // fetchAllFundingRates
86
+ // ──────────────────────────────────────────────
87
+ describe("fetchAllFundingRates", () => {
88
+ it("fetches from all 3 exchanges in parallel and compares", async () => {
89
+ setupMockFetch({
90
+ pac: [
91
+ { symbol: "BTC", funding: 0.0008, mark: 60000 },
92
+ { symbol: "ETH", funding: 0.0004, mark: 3000 },
93
+ ],
94
+ hl: {
95
+ assets: [{ name: "BTC" }, { name: "ETH" }],
96
+ ctxs: [
97
+ { funding: 0.0002, markPx: 60100 },
98
+ { funding: 0.0001, markPx: 3010 },
99
+ ],
100
+ },
101
+ lt: {
102
+ details: [
103
+ { market_id: 1, symbol: "BTC", last_trade_price: 59900 },
104
+ { market_id: 2, symbol: "ETH", last_trade_price: 2990 },
105
+ ],
106
+ funding: [
107
+ { market_id: 1, rate: 0.0005 },
108
+ { market_id: 2, rate: 0.0002 },
109
+ ],
110
+ },
111
+ });
112
+ const snapshot = await fetchAllFundingRates();
113
+ expect(snapshot.timestamp).toBeTruthy();
114
+ expect(snapshot.exchangeStatus.pacifica).toBe("ok");
115
+ expect(snapshot.exchangeStatus.hyperliquid).toBe("ok");
116
+ expect(snapshot.exchangeStatus.lighter).toBe("ok");
117
+ expect(snapshot.symbols.length).toBe(2);
118
+ // Should be sorted by spread descending
119
+ const btc = snapshot.symbols.find(s => s.symbol === "BTC");
120
+ expect(btc).toBeTruthy();
121
+ expect(btc.rates.length).toBe(3);
122
+ expect(btc.maxSpreadAnnual).toBeGreaterThan(0);
123
+ });
124
+ it("identifies correct long/short direction", async () => {
125
+ setupMockFetch({
126
+ pac: [{ symbol: "BTC", funding: 0.001, mark: 60000 }],
127
+ hl: {
128
+ assets: [{ name: "BTC" }],
129
+ ctxs: [{ funding: 0.00005, markPx: 60100 }],
130
+ },
131
+ });
132
+ const snapshot = await fetchAllFundingRates();
133
+ const btc = snapshot.symbols.find(s => s.symbol === "BTC");
134
+ expect(btc).toBeTruthy();
135
+ // PAC funding is higher -> short PAC (get paid), long HL (pay less)
136
+ // HL rate per hour = 0.00005, PAC rate per hour = 0.001
137
+ // HL hourly < PAC hourly -> long on HL, short on PAC
138
+ expect(btc.longExchange).toBe("hyperliquid");
139
+ expect(btc.shortExchange).toBe("pacifica");
140
+ });
141
+ it("filters by symbols when specified", async () => {
142
+ setupMockFetch({
143
+ pac: [
144
+ { symbol: "BTC", funding: 0.0008, mark: 60000 },
145
+ { symbol: "ETH", funding: 0.0004, mark: 3000 },
146
+ ],
147
+ hl: {
148
+ assets: [{ name: "BTC" }, { name: "ETH" }],
149
+ ctxs: [
150
+ { funding: 0.0001, markPx: 60100 },
151
+ { funding: 0.00005, markPx: 3010 },
152
+ ],
153
+ },
154
+ });
155
+ const snapshot = await fetchAllFundingRates({ symbols: ["BTC"] });
156
+ expect(snapshot.symbols.length).toBe(1);
157
+ expect(snapshot.symbols[0].symbol).toBe("BTC");
158
+ });
159
+ it("filters by minimum spread", async () => {
160
+ setupMockFetch({
161
+ pac: [
162
+ { symbol: "BTC", funding: 0.001, mark: 60000 }, // high spread
163
+ { symbol: "ETH", funding: 0.00011, mark: 3000 }, // tiny spread
164
+ ],
165
+ hl: {
166
+ assets: [{ name: "BTC" }, { name: "ETH" }],
167
+ ctxs: [
168
+ { funding: 0.00005, markPx: 60100 },
169
+ { funding: 0.0001, markPx: 3010 },
170
+ ],
171
+ },
172
+ });
173
+ const snapshot = await fetchAllFundingRates({ minSpread: 50 });
174
+ // Only BTC should have a spread > 50%
175
+ for (const s of snapshot.symbols) {
176
+ expect(s.maxSpreadAnnual).toBeGreaterThanOrEqual(50);
177
+ }
178
+ });
179
+ it("requires at least 2 exchanges for a symbol", async () => {
180
+ setupMockFetch({
181
+ pac: [{ symbol: "UNIQUE_PAC", funding: 0.001, mark: 100 }],
182
+ hl: { assets: [], ctxs: [] },
183
+ });
184
+ const snapshot = await fetchAllFundingRates();
185
+ // UNIQUE_PAC only on pacifica -> should be excluded
186
+ const unique = snapshot.symbols.find(s => s.symbol === "UNIQUE_PAC");
187
+ expect(unique).toBeUndefined();
188
+ });
189
+ it("handles exchange errors gracefully", async () => {
190
+ setupMockFetch({
191
+ pac: [{ symbol: "BTC", funding: 0.0008, mark: 60000 }],
192
+ hlError: true,
193
+ lt: {
194
+ details: [{ market_id: 1, symbol: "BTC", last_trade_price: 59900 }],
195
+ funding: [{ market_id: 1, rate: 0.0005 }],
196
+ },
197
+ });
198
+ const snapshot = await fetchAllFundingRates();
199
+ expect(snapshot.exchangeStatus.hyperliquid).toBe("error");
200
+ expect(snapshot.exchangeStatus.pacifica).toBe("ok");
201
+ expect(snapshot.exchangeStatus.lighter).toBe("ok");
202
+ // BTC should still be available (2 exchanges: pac + lt)
203
+ const btc = snapshot.symbols.find(s => s.symbol === "BTC");
204
+ expect(btc).toBeTruthy();
205
+ expect(btc.rates.length).toBe(2);
206
+ });
207
+ it("prefers HL mark price as most liquid", async () => {
208
+ setupMockFetch({
209
+ pac: [{ symbol: "BTC", funding: 0.0008, mark: 59000 }],
210
+ hl: {
211
+ assets: [{ name: "BTC" }],
212
+ ctxs: [{ funding: 0.0001, markPx: 60000 }],
213
+ },
214
+ lt: {
215
+ details: [{ market_id: 1, symbol: "BTC", last_trade_price: 59500 }],
216
+ funding: [{ market_id: 1, rate: 0.0005 }],
217
+ },
218
+ });
219
+ const snapshot = await fetchAllFundingRates();
220
+ const btc = snapshot.symbols.find(s => s.symbol === "BTC");
221
+ expect(btc.bestMarkPrice).toBe(60000); // HL price preferred
222
+ });
223
+ it("estimates positive hourly income for favorable spreads", async () => {
224
+ setupMockFetch({
225
+ pac: [{ symbol: "BTC", funding: 0.002, mark: 60000 }],
226
+ hl: {
227
+ assets: [{ name: "BTC" }],
228
+ ctxs: [{ funding: -0.0001, markPx: 60000 }],
229
+ },
230
+ });
231
+ const snapshot = await fetchAllFundingRates();
232
+ const btc = snapshot.symbols.find(s => s.symbol === "BTC");
233
+ // Large positive spread -> positive estimated income
234
+ expect(btc.estHourlyIncomeUsd).toBeGreaterThan(0);
235
+ });
236
+ it("returns results sorted by spread descending", async () => {
237
+ setupMockFetch({
238
+ pac: [
239
+ { symbol: "SMALL", funding: 0.0002, mark: 100 },
240
+ { symbol: "BIG", funding: 0.003, mark: 200 },
241
+ ],
242
+ hl: {
243
+ assets: [{ name: "SMALL" }, { name: "BIG" }],
244
+ ctxs: [
245
+ { funding: 0.0001, markPx: 100 },
246
+ { funding: 0.00005, markPx: 200 },
247
+ ],
248
+ },
249
+ });
250
+ const snapshot = await fetchAllFundingRates();
251
+ if (snapshot.symbols.length >= 2) {
252
+ expect(snapshot.symbols[0].maxSpreadAnnual).toBeGreaterThanOrEqual(snapshot.symbols[1].maxSpreadAnnual);
253
+ }
254
+ });
255
+ });
256
+ // ──────────────────────────────────────────────
257
+ // fetchSymbolFundingRates
258
+ // ──────────────────────────────────────────────
259
+ describe("fetchSymbolFundingRates", () => {
260
+ it("returns comparison for a single symbol", async () => {
261
+ setupMockFetch({
262
+ pac: [{ symbol: "ETH", funding: 0.0004, mark: 3000 }],
263
+ hl: {
264
+ assets: [{ name: "ETH" }, { name: "BTC" }],
265
+ ctxs: [
266
+ { funding: 0.0001, markPx: 3010 },
267
+ { funding: 0.0002, markPx: 60000 },
268
+ ],
269
+ },
270
+ });
271
+ const result = await fetchSymbolFundingRates("ETH");
272
+ expect(result).toBeTruthy();
273
+ expect(result.symbol).toBe("ETH");
274
+ expect(result.rates.length).toBe(2);
275
+ });
276
+ it("returns null when symbol not found on 2+ exchanges", async () => {
277
+ setupMockFetch({
278
+ pac: [],
279
+ hl: {
280
+ assets: [{ name: "NOEXIST" }],
281
+ ctxs: [{ funding: 0.0001, markPx: 100 }],
282
+ },
283
+ });
284
+ const result = await fetchSymbolFundingRates("NOEXIST");
285
+ expect(result).toBeNull();
286
+ });
287
+ it("is case-insensitive", async () => {
288
+ setupMockFetch({
289
+ pac: [{ symbol: "SOL", funding: 0.0005, mark: 150 }],
290
+ hl: {
291
+ assets: [{ name: "SOL" }],
292
+ ctxs: [{ funding: 0.0001, markPx: 151 }],
293
+ },
294
+ });
295
+ const result = await fetchSymbolFundingRates("sol");
296
+ expect(result).toBeTruthy();
297
+ expect(result.symbol).toBe("SOL");
298
+ });
299
+ });
300
+ // ──────────────────────────────────────────────
301
+ // 3-DEX direction logic
302
+ // ──────────────────────────────────────────────
303
+ describe("3-DEX direction logic", () => {
304
+ it("picks correct long/short when lighter has best rate", async () => {
305
+ setupMockFetch({
306
+ pac: [{ symbol: "BTC", funding: 0.00006, mark: 60000 }], // hourly = 0.00006
307
+ hl: {
308
+ assets: [{ name: "BTC" }],
309
+ ctxs: [{ funding: 0.0002, markPx: 60100 }], // hourly = 0.0002 (highest)
310
+ },
311
+ lt: {
312
+ details: [{ market_id: 1, symbol: "BTC", last_trade_price: 59900 }],
313
+ funding: [{ market_id: 1, rate: 0.00001 }], // 8h rate, hourly = 0.00001/8 (lowest)
314
+ },
315
+ });
316
+ const snapshot = await fetchAllFundingRates();
317
+ const btc = snapshot.symbols.find(s => s.symbol === "BTC");
318
+ expect(btc).toBeTruthy();
319
+ // Lighter has lowest hourly rate (0.00001/8) -> long on lighter
320
+ // HL has highest hourly rate (0.0002) -> short on HL
321
+ expect(btc.longExchange).toBe("lighter");
322
+ expect(btc.shortExchange).toBe("hyperliquid");
323
+ });
324
+ it("picks pacifica as short when it has highest rate", async () => {
325
+ setupMockFetch({
326
+ pac: [{ symbol: "ETH", funding: 0.0005, mark: 3000 }], // hourly = 0.0005 (highest)
327
+ hl: {
328
+ assets: [{ name: "ETH" }],
329
+ ctxs: [{ funding: 0.0001, markPx: 3010 }], // hourly = 0.0001 (mid)
330
+ },
331
+ lt: {
332
+ details: [{ market_id: 2, symbol: "ETH", last_trade_price: 2990 }],
333
+ funding: [{ market_id: 2, rate: 0.000025 }], // 8h rate, hourly = 0.000025/8 (lowest)
334
+ },
335
+ });
336
+ const snapshot = await fetchAllFundingRates();
337
+ const eth = snapshot.symbols.find(s => s.symbol === "ETH");
338
+ expect(eth).toBeTruthy();
339
+ expect(eth.shortExchange).toBe("pacifica");
340
+ expect(eth.longExchange).toBe("lighter");
341
+ });
342
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,173 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { getFundingHours, toHourlyRate, annualizeRate, computeAnnualSpread, estimateHourlyFunding, } from "../funding.js";
3
+ // ──────────────────────────────────────────────
4
+ // getFundingHours
5
+ // ──────────────────────────────────────────────
6
+ describe("getFundingHours", () => {
7
+ it("returns 1 for hyperliquid", () => {
8
+ expect(getFundingHours("hyperliquid")).toBe(1);
9
+ });
10
+ it("returns 1 for pacifica", () => {
11
+ expect(getFundingHours("pacifica")).toBe(1);
12
+ });
13
+ it("returns 8 for lighter", () => {
14
+ expect(getFundingHours("lighter")).toBe(8);
15
+ });
16
+ it("defaults to 1 for unknown exchanges (main exchanges are hourly)", () => {
17
+ expect(getFundingHours("binance")).toBe(1);
18
+ expect(getFundingHours("unknown_dex")).toBe(1);
19
+ });
20
+ it("is case-insensitive", () => {
21
+ expect(getFundingHours("Hyperliquid")).toBe(1);
22
+ expect(getFundingHours("PACIFICA")).toBe(1);
23
+ expect(getFundingHours("Lighter")).toBe(8);
24
+ });
25
+ });
26
+ // ──────────────────────────────────────────────
27
+ // toHourlyRate
28
+ // ──────────────────────────────────────────────
29
+ describe("toHourlyRate", () => {
30
+ it("divides by 1 for hyperliquid (rate is already per-hour)", () => {
31
+ const hourly = toHourlyRate(0.0001, "hyperliquid");
32
+ expect(hourly).toBeCloseTo(0.0001);
33
+ });
34
+ it("divides by 1 for pacifica (rate is already per-hour)", () => {
35
+ const hourly = toHourlyRate(0.0001, "pacifica");
36
+ expect(hourly).toBeCloseTo(0.0001);
37
+ });
38
+ it("divides by 8 for lighter (API returns 8h rate)", () => {
39
+ const hourly = toHourlyRate(0.0002, "lighter");
40
+ expect(hourly).toBeCloseTo(0.0002 / 8);
41
+ });
42
+ it("divides by 1 for unknown exchanges (default)", () => {
43
+ const hourly = toHourlyRate(0.0001, "someExchange");
44
+ expect(hourly).toBeCloseTo(0.0001);
45
+ });
46
+ it("handles zero rate", () => {
47
+ expect(toHourlyRate(0, "hyperliquid")).toBe(0);
48
+ expect(toHourlyRate(0, "pacifica")).toBe(0);
49
+ });
50
+ it("handles negative rate", () => {
51
+ const hourly = toHourlyRate(-0.0001, "pacifica");
52
+ expect(hourly).toBeCloseTo(-0.0001);
53
+ });
54
+ });
55
+ // ──────────────────────────────────────────────
56
+ // annualizeRate
57
+ // ──────────────────────────────────────────────
58
+ describe("annualizeRate", () => {
59
+ it("annualizes hyperliquid rate (hourly * 8760 * 100)", () => {
60
+ // rate = 0.0001 per hour → annualized = 0.0001 * 8760 * 100 = 87.6%
61
+ const annual = annualizeRate(0.0001, "hyperliquid");
62
+ expect(annual).toBeCloseTo(87.6);
63
+ });
64
+ it("annualizes pacifica rate (hourly * 8760 * 100)", () => {
65
+ // rate = 0.0001 per hour → annualized = 0.0001 * 8760 * 100 = 87.6%
66
+ const annual = annualizeRate(0.0001, "pacifica");
67
+ expect(annual).toBeCloseTo(87.6);
68
+ });
69
+ it("produces same annualized rate for equivalent rates across exchanges", () => {
70
+ // All exchanges are hourly now, so same rate = same annualized
71
+ const hlAnnual = annualizeRate(0.0001, "hyperliquid");
72
+ const pacAnnual = annualizeRate(0.0001, "pacifica");
73
+ expect(hlAnnual).toBeCloseTo(pacAnnual);
74
+ });
75
+ it("handles zero rate", () => {
76
+ expect(annualizeRate(0, "hyperliquid")).toBe(0);
77
+ expect(annualizeRate(0, "pacifica")).toBe(0);
78
+ });
79
+ it("handles negative rates", () => {
80
+ const annual = annualizeRate(-0.0001, "hyperliquid");
81
+ expect(annual).toBeCloseTo(-87.6);
82
+ });
83
+ });
84
+ // ──────────────────────────────────────────────
85
+ // computeAnnualSpread
86
+ // ──────────────────────────────────────────────
87
+ describe("computeAnnualSpread", () => {
88
+ it("computes spread between two different exchanges", () => {
89
+ // HL rate 0.0002/h, pacifica rate 0.0001/h → spread = 0.0001/h * 8760 * 100 = 87.6%
90
+ const spread = computeAnnualSpread(0.0002, "hyperliquid", 0.0001, "pacifica");
91
+ expect(spread).toBeCloseTo(87.6);
92
+ });
93
+ it("returns 0 when rates are identical", () => {
94
+ // Both hourly, same rate
95
+ const spread = computeAnnualSpread(0.0001, "hyperliquid", 0.0001, "pacifica");
96
+ expect(spread).toBeCloseTo(0);
97
+ });
98
+ it("returns absolute value regardless of which rate is higher", () => {
99
+ const spread1 = computeAnnualSpread(0.0003, "hyperliquid", 0.0001, "hyperliquid");
100
+ const spread2 = computeAnnualSpread(0.0001, "hyperliquid", 0.0003, "hyperliquid");
101
+ expect(spread1).toBeCloseTo(spread2);
102
+ expect(spread1).toBeGreaterThan(0);
103
+ });
104
+ it("computes spread between pacifica (1h) and lighter (8h)", () => {
105
+ // pacifica raw 0.000125 → hourly = 0.000125
106
+ // lighter raw 0.0000625 → hourly = 0.0000625/8 = 0.0000078125
107
+ // diff: |0.000125 - 0.0000078125| = 0.0001171875/h * 8760 * 100 = 102.66%
108
+ const spread = computeAnnualSpread(0.000125, "pacifica", 0.0000625, "lighter");
109
+ expect(spread).toBeCloseTo(102.66, 1);
110
+ });
111
+ it("handles zero rates", () => {
112
+ const spread = computeAnnualSpread(0, "hyperliquid", 0, "pacifica");
113
+ expect(spread).toBe(0);
114
+ });
115
+ it("handles one zero rate", () => {
116
+ const spread = computeAnnualSpread(0.0001, "hyperliquid", 0, "pacifica");
117
+ // hourly diff = 0.0001, spread = 0.0001 * 8760 * 100 = 87.6%
118
+ expect(spread).toBeCloseTo(87.6);
119
+ });
120
+ it("handles negative rates (one exchange paying, other receiving)", () => {
121
+ // HL pays +0.0001/h, pacifica -0.0001/h
122
+ // diff = |0.0001 - (-0.0001)| = 0.0002/h * 8760 * 100 = 175.2%
123
+ const spread = computeAnnualSpread(0.0001, "hyperliquid", -0.0001, "pacifica");
124
+ expect(spread).toBeCloseTo(175.2);
125
+ });
126
+ });
127
+ // ──────────────────────────────────────────────
128
+ // estimateHourlyFunding
129
+ // ──────────────────────────────────────────────
130
+ describe("estimateHourlyFunding", () => {
131
+ it("long pays positive funding (positive rate)", () => {
132
+ // rate = 0.0001/h (HL), position = $10000
133
+ // hourly payment = 0.0001 * 10000 * 1 = $1
134
+ const payment = estimateHourlyFunding(0.0001, "hyperliquid", 10000, "long");
135
+ expect(payment).toBeCloseTo(1);
136
+ });
137
+ it("short receives positive funding (positive rate)", () => {
138
+ // rate = 0.0001/h (HL), position = $10000
139
+ // hourly payment = 0.0001 * 10000 * (-1) = -$1 (receiving)
140
+ const payment = estimateHourlyFunding(0.0001, "hyperliquid", 10000, "short");
141
+ expect(payment).toBeCloseTo(-1);
142
+ });
143
+ it("long receives negative funding (negative rate)", () => {
144
+ // rate = -0.0001/h, position = $10000
145
+ // hourly = -0.0001 * 10000 * 1 = -$1 (receiving)
146
+ const payment = estimateHourlyFunding(-0.0001, "hyperliquid", 10000, "long");
147
+ expect(payment).toBeCloseTo(-1);
148
+ });
149
+ it("short pays negative funding (negative rate)", () => {
150
+ // rate = -0.0001/h, position = $10000
151
+ // hourly = -0.0001 * 10000 * (-1) = $1 (paying)
152
+ const payment = estimateHourlyFunding(-0.0001, "hyperliquid", 10000, "short");
153
+ expect(payment).toBeCloseTo(1);
154
+ });
155
+ it("uses hourly rate directly for pacifica", () => {
156
+ // rate = 0.0001/h (pacifica), position = $10000
157
+ // long pays: 0.0001 * 10000 = $1
158
+ const payment = estimateHourlyFunding(0.0001, "pacifica", 10000, "long");
159
+ expect(payment).toBeCloseTo(1);
160
+ });
161
+ it("returns 0 for zero funding rate", () => {
162
+ expect(estimateHourlyFunding(0, "hyperliquid", 10000, "long")).toBeCloseTo(0);
163
+ expect(estimateHourlyFunding(0, "pacifica", 10000, "short")).toBeCloseTo(0);
164
+ });
165
+ it("returns 0 for zero position size", () => {
166
+ expect(estimateHourlyFunding(0.0001, "hyperliquid", 0, "long")).toBe(0);
167
+ });
168
+ it("scales linearly with position size", () => {
169
+ const small = estimateHourlyFunding(0.0001, "hyperliquid", 1000, "long");
170
+ const large = estimateHourlyFunding(0.0001, "hyperliquid", 10000, "long");
171
+ expect(large).toBeCloseTo(small * 10);
172
+ });
173
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from "vitest";
2
+ function computeGap(symbol, pacPrice, hlPrice) {
3
+ if (pacPrice <= 0 || hlPrice <= 0)
4
+ return null;
5
+ const mid = (pacPrice + hlPrice) / 2;
6
+ const gapPct = ((pacPrice - hlPrice) / mid) * 100;
7
+ return {
8
+ symbol,
9
+ pacPrice,
10
+ hlPrice,
11
+ gapPct,
12
+ direction: pacPrice > hlPrice ? "PAC>HL" : "HL>PAC",
13
+ };
14
+ }
15
+ describe("Price gap computation", () => {
16
+ it("positive gap when PAC > HL", () => {
17
+ const gap = computeGap("BTC", 100100, 99900);
18
+ expect(gap).not.toBeNull();
19
+ expect(gap.gapPct).toBeCloseTo(0.2, 1);
20
+ expect(gap.direction).toBe("PAC>HL");
21
+ });
22
+ it("negative gap when HL > PAC", () => {
23
+ const gap = computeGap("ETH", 3490, 3510);
24
+ expect(gap).not.toBeNull();
25
+ expect(gap.gapPct).toBeLessThan(0);
26
+ expect(gap.direction).toBe("HL>PAC");
27
+ });
28
+ it("zero gap when equal", () => {
29
+ const gap = computeGap("SOL", 150, 150);
30
+ expect(gap).not.toBeNull();
31
+ expect(gap.gapPct).toBe(0);
32
+ });
33
+ it("returns null for zero prices", () => {
34
+ expect(computeGap("X", 0, 100)).toBeNull();
35
+ expect(computeGap("X", 100, 0)).toBeNull();
36
+ });
37
+ it("handles large gaps", () => {
38
+ const gap = computeGap("MEME", 1.0, 0.5);
39
+ expect(gap).not.toBeNull();
40
+ // (1.0 - 0.5) / 0.75 * 100 ≈ 66.67%
41
+ expect(gap.gapPct).toBeCloseTo(66.67, 1);
42
+ });
43
+ });
@@ -0,0 +1 @@
1
+ export {};