perp-cli 0.11.0 → 0.12.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 (240) hide show
  1. package/README.md +30 -25
  2. package/dist/_stderr-filter.d.ts +0 -0
  3. package/dist/_stderr-filter.js +30 -0
  4. package/dist/agent-wallet/expiry.d.ts +15 -0
  5. package/dist/agent-wallet/expiry.js +26 -0
  6. package/dist/agent-wallet/lighter-keystore.d.ts +34 -0
  7. package/dist/agent-wallet/lighter-keystore.js +232 -0
  8. package/dist/agent-wallet/passphrase.d.ts +13 -0
  9. package/dist/agent-wallet/passphrase.js +54 -0
  10. package/dist/agent-wallet/signing-strategy.d.ts +67 -0
  11. package/dist/agent-wallet/signing-strategy.js +51 -0
  12. package/dist/agent-wallet/store.d.ts +37 -0
  13. package/dist/agent-wallet/store.js +172 -0
  14. package/dist/api/public/_http.d.ts +15 -0
  15. package/dist/api/public/_http.js +24 -0
  16. package/dist/api/public/hyperliquid.js +36 -24
  17. package/dist/api/public/lighter.d.ts +7 -1
  18. package/dist/api/public/lighter.js +85 -37
  19. package/dist/api/public/pacifica.js +47 -18
  20. package/dist/bridge-engine.js +102 -49
  21. package/dist/cli-spec.js +1 -2
  22. package/dist/commands/account.js +2 -500
  23. package/dist/commands/agent.d.ts +8 -2
  24. package/dist/commands/agent.js +1903 -366
  25. package/dist/commands/alerts.js +3 -1
  26. package/dist/commands/arb/index.js +37 -7
  27. package/dist/commands/arb-auto.js +44 -11
  28. package/dist/commands/arb.js +83 -34
  29. package/dist/commands/bot.d.ts +1 -1
  30. package/dist/commands/bot.js +18 -14
  31. package/dist/commands/bridge.d.ts +1 -1
  32. package/dist/commands/bridge.js +4 -4
  33. package/dist/commands/funds.d.ts +1 -1
  34. package/dist/commands/funds.js +18 -68
  35. package/dist/commands/jobs.d.ts +1 -1
  36. package/dist/commands/jobs.js +2 -2
  37. package/dist/commands/manage.d.ts +1 -1
  38. package/dist/commands/manage.js +12 -84
  39. package/dist/commands/plan.d.ts +1 -1
  40. package/dist/commands/plan.js +2 -2
  41. package/dist/commands/portfolio.d.ts +19 -3
  42. package/dist/commands/portfolio.js +747 -163
  43. package/dist/commands/rebalance.d.ts +1 -1
  44. package/dist/commands/rebalance.js +18 -12
  45. package/dist/commands/risk.js +2 -6
  46. package/dist/commands/settings.js +30 -53
  47. package/dist/commands/trade.js +16 -15
  48. package/dist/commands/wallet.d.ts +2 -1
  49. package/dist/commands/wallet.js +756 -28
  50. package/dist/errors.d.ts +64 -0
  51. package/dist/errors.js +82 -2
  52. package/dist/event-stream.js +12 -1
  53. package/dist/exchanges/aster-typed-data.d.ts +156 -0
  54. package/dist/exchanges/aster-typed-data.js +189 -0
  55. package/dist/exchanges/aster.d.ts +60 -11
  56. package/dist/exchanges/aster.js +278 -136
  57. package/dist/exchanges/hyperliquid.d.ts +14 -0
  58. package/dist/exchanges/hyperliquid.js +84 -34
  59. package/dist/exchanges/lighter-spot.js +14 -3
  60. package/dist/exchanges/lighter.d.ts +66 -0
  61. package/dist/exchanges/lighter.js +199 -27
  62. package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
  63. package/dist/exchanges/pacifica-typed-data.js +74 -0
  64. package/dist/exchanges/pacifica.d.ts +29 -1
  65. package/dist/exchanges/pacifica.js +94 -6
  66. package/dist/funding/rates.js +59 -21
  67. package/dist/guardrail/perp-guardrail.js +9 -1
  68. package/dist/index.d.ts +1 -1
  69. package/dist/index.js +255 -360
  70. package/dist/jobs.js +3 -1
  71. package/dist/mcp-server.js +85 -83
  72. package/dist/position-history.js +47 -4
  73. package/dist/settings.d.ts +67 -3
  74. package/dist/settings.js +1 -2
  75. package/dist/signer/ows-evm.d.ts +14 -0
  76. package/dist/signer/ows-evm.js +28 -9
  77. package/dist/smart-order.d.ts +8 -2
  78. package/dist/smart-order.js +46 -11
  79. package/dist/strategies/funding-arb.js +29 -8
  80. package/dist/trade-validator.js +30 -5
  81. package/dist/utils.d.ts +10 -0
  82. package/dist/utils.js +19 -0
  83. package/package.json +2 -2
  84. package/skills/perp-cli/SKILL.md +23 -14
  85. package/skills/perp-cli/references/agent-operations.md +6 -4
  86. package/skills/perp-cli/references/commands.md +55 -34
  87. package/skills/perp-cli/scripts/preflight.sh +4 -4
  88. package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
  89. package/dist/__tests__/alert-logic.test.d.ts +0 -1
  90. package/dist/__tests__/alert-logic.test.js +0 -107
  91. package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
  92. package/dist/__tests__/arb-auto-3dex.test.js +0 -397
  93. package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
  94. package/dist/__tests__/arb-history-stats.test.js +0 -176
  95. package/dist/__tests__/arb-logic.test.d.ts +0 -1
  96. package/dist/__tests__/arb-logic.test.js +0 -84
  97. package/dist/__tests__/arb-manage.test.d.ts +0 -1
  98. package/dist/__tests__/arb-manage.test.js +0 -253
  99. package/dist/__tests__/arb-new-features.test.d.ts +0 -1
  100. package/dist/__tests__/arb-new-features.test.js +0 -457
  101. package/dist/__tests__/arb-sizing.test.d.ts +0 -1
  102. package/dist/__tests__/arb-sizing.test.js +0 -48
  103. package/dist/__tests__/arb-state.test.d.ts +0 -1
  104. package/dist/__tests__/arb-state.test.js +0 -284
  105. package/dist/__tests__/arb-userflow.test.d.ts +0 -1
  106. package/dist/__tests__/arb-userflow.test.js +0 -945
  107. package/dist/__tests__/arb-utils.test.d.ts +0 -1
  108. package/dist/__tests__/arb-utils.test.js +0 -264
  109. package/dist/__tests__/bot-conditions.test.d.ts +0 -1
  110. package/dist/__tests__/bot-conditions.test.js +0 -341
  111. package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
  112. package/dist/__tests__/client-id-tracker.test.js +0 -137
  113. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
  114. package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
  115. package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
  116. package/dist/__tests__/commands/order-intent.test.js +0 -600
  117. package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
  118. package/dist/__tests__/commands/trade-commands.test.js +0 -821
  119. package/dist/__tests__/config.test.d.ts +0 -1
  120. package/dist/__tests__/config.test.js +0 -86
  121. package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
  122. package/dist/__tests__/cross-chain-margin.test.js +0 -287
  123. package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
  124. package/dist/__tests__/dex-asset-map.test.js +0 -191
  125. package/dist/__tests__/errors.test.d.ts +0 -1
  126. package/dist/__tests__/errors.test.js +0 -110
  127. package/dist/__tests__/event-stream.test.d.ts +0 -1
  128. package/dist/__tests__/event-stream.test.js +0 -276
  129. package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
  130. package/dist/__tests__/exchanges/interface.test.js +0 -132
  131. package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
  132. package/dist/__tests__/exchanges/mock-adapter.js +0 -137
  133. package/dist/__tests__/execution-log.test.d.ts +0 -1
  134. package/dist/__tests__/execution-log.test.js +0 -106
  135. package/dist/__tests__/funding-calc.test.d.ts +0 -1
  136. package/dist/__tests__/funding-calc.test.js +0 -71
  137. package/dist/__tests__/funding-history.test.d.ts +0 -1
  138. package/dist/__tests__/funding-history.test.js +0 -343
  139. package/dist/__tests__/funding-rates.test.d.ts +0 -1
  140. package/dist/__tests__/funding-rates.test.js +0 -342
  141. package/dist/__tests__/funding.test.d.ts +0 -1
  142. package/dist/__tests__/funding.test.js +0 -173
  143. package/dist/__tests__/gap-logic.test.d.ts +0 -1
  144. package/dist/__tests__/gap-logic.test.js +0 -43
  145. package/dist/__tests__/hip3-dex.test.d.ts +0 -1
  146. package/dist/__tests__/hip3-dex.test.js +0 -234
  147. package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
  148. package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
  149. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
  150. package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
  151. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
  152. package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
  153. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
  154. package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
  155. package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
  156. package/dist/__tests__/integration/bridge.integration.test.js +0 -309
  157. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
  158. package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
  159. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
  160. package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
  161. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
  162. package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
  163. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
  164. package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
  165. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
  166. package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
  167. package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
  168. package/dist/__tests__/integration/lighter.integration.test.js +0 -53
  169. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
  170. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
  171. package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
  172. package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
  173. package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
  174. package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
  175. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
  176. package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
  177. package/dist/__tests__/liquidity.test.d.ts +0 -1
  178. package/dist/__tests__/liquidity.test.js +0 -225
  179. package/dist/__tests__/plan-executor.test.d.ts +0 -1
  180. package/dist/__tests__/plan-executor.test.js +0 -314
  181. package/dist/__tests__/position-history.test.d.ts +0 -1
  182. package/dist/__tests__/position-history.test.js +0 -367
  183. package/dist/__tests__/retry.test.d.ts +0 -1
  184. package/dist/__tests__/retry.test.js +0 -310
  185. package/dist/__tests__/risk-assessment.test.d.ts +0 -1
  186. package/dist/__tests__/risk-assessment.test.js +0 -288
  187. package/dist/__tests__/security-adversarial.test.d.ts +0 -1
  188. package/dist/__tests__/security-adversarial.test.js +0 -574
  189. package/dist/__tests__/strategies.test.d.ts +0 -1
  190. package/dist/__tests__/strategies.test.js +0 -539
  191. package/dist/__tests__/trade-execution.test.d.ts +0 -1
  192. package/dist/__tests__/trade-execution.test.js +0 -129
  193. package/dist/__tests__/trade-validator.test.d.ts +0 -1
  194. package/dist/__tests__/trade-validator.test.js +0 -655
  195. package/dist/__tests__/utils.test.d.ts +0 -1
  196. package/dist/__tests__/utils.test.js +0 -76
  197. package/dist/arb/reconcile.d.ts +0 -28
  198. package/dist/arb/reconcile.js +0 -162
  199. package/dist/commands/alert.d.ts +0 -2
  200. package/dist/commands/alert.js +0 -421
  201. package/dist/commands/analytics.d.ts +0 -3
  202. package/dist/commands/analytics.js +0 -311
  203. package/dist/commands/arb-manage.d.ts +0 -5
  204. package/dist/commands/arb-manage.js +0 -5
  205. package/dist/commands/dashboard.d.ts +0 -3
  206. package/dist/commands/dashboard.js +0 -176
  207. package/dist/commands/deposit.d.ts +0 -4
  208. package/dist/commands/deposit.js +0 -573
  209. package/dist/commands/dex.d.ts +0 -3
  210. package/dist/commands/dex.js +0 -120
  211. package/dist/commands/env.d.ts +0 -2
  212. package/dist/commands/env.js +0 -136
  213. package/dist/commands/funding.d.ts +0 -3
  214. package/dist/commands/funding.js +0 -554
  215. package/dist/commands/gap.d.ts +0 -2
  216. package/dist/commands/gap.js +0 -305
  217. package/dist/commands/health.d.ts +0 -2
  218. package/dist/commands/health.js +0 -67
  219. package/dist/commands/multileg.d.ts +0 -16
  220. package/dist/commands/multileg.js +0 -174
  221. package/dist/commands/ows.d.ts +0 -2
  222. package/dist/commands/ows.js +0 -887
  223. package/dist/commands/pnl.d.ts +0 -12
  224. package/dist/commands/pnl.js +0 -243
  225. package/dist/commands/run.d.ts +0 -3
  226. package/dist/commands/run.js +0 -202
  227. package/dist/commands/stream.d.ts +0 -5
  228. package/dist/commands/stream.js +0 -200
  229. package/dist/commands/withdraw.d.ts +0 -3
  230. package/dist/commands/withdraw.js +0 -187
  231. package/dist/exchanges/edgex.d.ts +0 -62
  232. package/dist/exchanges/edgex.js +0 -552
  233. package/dist/exchanges/nado.d.ts +0 -52
  234. package/dist/exchanges/nado.js +0 -223
  235. package/dist/funding/index.d.ts +0 -3
  236. package/dist/funding/index.js +0 -3
  237. package/dist/retry.d.ts +0 -74
  238. package/dist/retry.js +0 -129
  239. package/dist/ws/market-feed.d.ts +0 -74
  240. package/dist/ws/market-feed.js +0 -227
@@ -1 +0,0 @@
1
- export {};
@@ -1,342 +0,0 @@
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
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,173 +0,0 @@
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
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,43 +0,0 @@
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
- });
@@ -1 +0,0 @@
1
- export {};