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,86 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { loadPrivateKey, parseSolanaKeypair, isEvmPrivateKey } from "../config.js";
3
+ import { Keypair } from "@solana/web3.js";
4
+ import bs58 from "bs58";
5
+ describe("isEvmPrivateKey", () => {
6
+ it("returns true for valid EVM private key", () => {
7
+ const key = "0x" + "a".repeat(64);
8
+ expect(isEvmPrivateKey(key)).toBe(true);
9
+ });
10
+ it("returns false without 0x prefix", () => {
11
+ expect(isEvmPrivateKey("a".repeat(64))).toBe(false);
12
+ });
13
+ it("returns false for wrong length", () => {
14
+ expect(isEvmPrivateKey("0x" + "a".repeat(32))).toBe(false);
15
+ });
16
+ });
17
+ describe("parseSolanaKeypair", () => {
18
+ it("parses base58-encoded private key", () => {
19
+ const kp = Keypair.generate();
20
+ const b58 = bs58.encode(kp.secretKey);
21
+ const parsed = parseSolanaKeypair(b58);
22
+ expect(parsed.publicKey.toBase58()).toBe(kp.publicKey.toBase58());
23
+ });
24
+ it("parses JSON byte array format", () => {
25
+ const kp = Keypair.generate();
26
+ const jsonArr = JSON.stringify(Array.from(kp.secretKey));
27
+ const parsed = parseSolanaKeypair(jsonArr);
28
+ expect(parsed.publicKey.toBase58()).toBe(kp.publicKey.toBase58());
29
+ });
30
+ it("throws for invalid input", () => {
31
+ expect(() => parseSolanaKeypair("not-a-valid-key")).toThrow("Invalid Solana private key");
32
+ });
33
+ });
34
+ describe("loadPrivateKey", () => {
35
+ const originalEnv = { ...process.env };
36
+ beforeEach(() => {
37
+ // Clear relevant env vars
38
+ delete process.env.PACIFICA_PRIVATE_KEY;
39
+ delete process.env.pk;
40
+ delete process.env.HYPERLIQUID_PRIVATE_KEY;
41
+ delete process.env.HL_PRIVATE_KEY;
42
+ delete process.env.LIGHTER_PRIVATE_KEY;
43
+ delete process.env.PRIVATE_KEY;
44
+ });
45
+ afterEach(() => {
46
+ process.env = { ...originalEnv };
47
+ });
48
+ it("returns pkOverride when provided", async () => {
49
+ const result = await loadPrivateKey("pacifica", "my-override-key");
50
+ expect(result).toBe("my-override-key");
51
+ });
52
+ it("reads exchange-specific env var for pacifica", async () => {
53
+ process.env.PACIFICA_PRIVATE_KEY = "pac-key-123";
54
+ const result = await loadPrivateKey("pacifica");
55
+ expect(result).toBe("pac-key-123");
56
+ });
57
+ it("reads exchange-specific env var for hyperliquid", async () => {
58
+ process.env.HYPERLIQUID_PRIVATE_KEY = "hl-key-456";
59
+ const result = await loadPrivateKey("hyperliquid");
60
+ expect(result).toBe("hl-key-456");
61
+ });
62
+ it("reads HL_PRIVATE_KEY for hyperliquid", async () => {
63
+ process.env.HL_PRIVATE_KEY = "hl-alt-key";
64
+ const result = await loadPrivateKey("hyperliquid");
65
+ expect(result).toBe("hl-alt-key");
66
+ });
67
+ it("reads LIGHTER_PRIVATE_KEY for lighter", async () => {
68
+ process.env.LIGHTER_PRIVATE_KEY = "lighter-key-789";
69
+ const result = await loadPrivateKey("lighter");
70
+ expect(result).toBe("lighter-key-789");
71
+ });
72
+ it("falls back to PRIVATE_KEY", async () => {
73
+ process.env.PRIVATE_KEY = "generic-key";
74
+ const result = await loadPrivateKey("pacifica");
75
+ expect(result).toBe("generic-key");
76
+ });
77
+ it("throws when no key is found", async () => {
78
+ await expect(loadPrivateKey("pacifica")).rejects.toThrow("No private key configured");
79
+ });
80
+ it("prefers exchange-specific over generic", async () => {
81
+ process.env.PACIFICA_PRIVATE_KEY = "specific";
82
+ process.env.PRIVATE_KEY = "generic";
83
+ const result = await loadPrivateKey("pacifica");
84
+ expect(result).toBe("specific");
85
+ });
86
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,287 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { checkChainMargins, isCriticalMargin, shouldBlockEntries, computeAutoSize, } from "../cross-chain-margin.js";
3
+ // ── Mock adapter factory ──
4
+ function mockAdapter(opts) {
5
+ const bal = {
6
+ equity: String(opts.equity),
7
+ available: String(opts.available ?? opts.equity - opts.marginUsed),
8
+ marginUsed: String(opts.marginUsed),
9
+ unrealizedPnl: "0",
10
+ };
11
+ return {
12
+ name: opts.name,
13
+ getBalance: vi.fn().mockResolvedValue(bal),
14
+ getOrderbook: vi.fn().mockResolvedValue({
15
+ asks: opts.asks ?? [["100", "10"], ["101", "5"], ["102", "3"]],
16
+ bids: opts.bids ?? [["99", "8"], ["98", "6"], ["97", "4"]],
17
+ }),
18
+ getMarkets: vi.fn(),
19
+ getRecentTrades: vi.fn(),
20
+ getFundingHistory: vi.fn(),
21
+ getKlines: vi.fn(),
22
+ getPositions: vi.fn(),
23
+ getOpenOrders: vi.fn(),
24
+ getOrderHistory: vi.fn(),
25
+ getTradeHistory: vi.fn(),
26
+ getFundingPayments: vi.fn(),
27
+ marketOrder: vi.fn(),
28
+ limitOrder: vi.fn(),
29
+ editOrder: vi.fn(),
30
+ cancelOrder: vi.fn(),
31
+ cancelAllOrders: vi.fn(),
32
+ setLeverage: vi.fn(),
33
+ stopOrder: vi.fn(),
34
+ };
35
+ }
36
+ // ── checkChainMargins ──
37
+ describe("checkChainMargins", () => {
38
+ it("returns correct margin status for healthy exchange", async () => {
39
+ const adapters = new Map();
40
+ adapters.set("hyperliquid", mockAdapter({ name: "hyperliquid", equity: 1000, marginUsed: 200 }));
41
+ const statuses = await checkChainMargins(adapters, 30);
42
+ expect(statuses).toHaveLength(1);
43
+ expect(statuses[0].exchange).toBe("hyperliquid");
44
+ expect(statuses[0].chain).toBe("hyperliquid");
45
+ expect(statuses[0].equity).toBe(1000);
46
+ expect(statuses[0].usedMargin).toBe(200);
47
+ expect(statuses[0].freeMargin).toBe(800);
48
+ // marginRatio = (800/1000)*100 = 80%
49
+ expect(statuses[0].marginRatio).toBe(80);
50
+ expect(statuses[0].belowThreshold).toBe(false);
51
+ });
52
+ it("detects low margin below threshold", async () => {
53
+ const adapters = new Map();
54
+ adapters.set("lighter", mockAdapter({ name: "lighter", equity: 1000, marginUsed: 800 }));
55
+ const statuses = await checkChainMargins(adapters, 30);
56
+ expect(statuses).toHaveLength(1);
57
+ // marginRatio = (200/1000)*100 = 20% < 30%
58
+ expect(statuses[0].marginRatio).toBe(20);
59
+ expect(statuses[0].belowThreshold).toBe(true);
60
+ });
61
+ it("handles multiple exchanges", async () => {
62
+ const adapters = new Map();
63
+ adapters.set("hyperliquid", mockAdapter({ name: "hyperliquid", equity: 1000, marginUsed: 100 }));
64
+ adapters.set("pacifica", mockAdapter({ name: "pacifica", equity: 500, marginUsed: 400 }));
65
+ adapters.set("lighter", mockAdapter({ name: "lighter", equity: 2000, marginUsed: 500 }));
66
+ const statuses = await checkChainMargins(adapters, 30);
67
+ expect(statuses).toHaveLength(3);
68
+ const hl = statuses.find(s => s.exchange === "hyperliquid");
69
+ expect(hl.marginRatio).toBe(90); // (900/1000)*100
70
+ expect(hl.belowThreshold).toBe(false);
71
+ expect(hl.chain).toBe("hyperliquid");
72
+ const pac = statuses.find(s => s.exchange === "pacifica");
73
+ expect(pac.marginRatio).toBe(20); // (100/500)*100
74
+ expect(pac.belowThreshold).toBe(true);
75
+ expect(pac.chain).toBe("solana");
76
+ const lt = statuses.find(s => s.exchange === "lighter");
77
+ expect(lt.marginRatio).toBe(75); // (1500/2000)*100
78
+ expect(lt.belowThreshold).toBe(false);
79
+ expect(lt.chain).toBe("arbitrum");
80
+ });
81
+ it("handles zero equity", async () => {
82
+ const adapters = new Map();
83
+ adapters.set("hyperliquid", mockAdapter({ name: "hyperliquid", equity: 0, marginUsed: 0 }));
84
+ const statuses = await checkChainMargins(adapters, 30);
85
+ expect(statuses[0].marginRatio).toBe(0);
86
+ expect(statuses[0].belowThreshold).toBe(true);
87
+ });
88
+ });
89
+ // ── isCriticalMargin ──
90
+ describe("isCriticalMargin", () => {
91
+ it("returns true when margin ratio below 15%", () => {
92
+ const status = {
93
+ exchange: "test",
94
+ chain: "arbitrum",
95
+ equity: 1000,
96
+ usedMargin: 900,
97
+ freeMargin: 100,
98
+ marginRatio: 10,
99
+ belowThreshold: true,
100
+ };
101
+ expect(isCriticalMargin(status)).toBe(true);
102
+ });
103
+ it("returns false when margin ratio above 15%", () => {
104
+ const status = {
105
+ exchange: "test",
106
+ chain: "arbitrum",
107
+ equity: 1000,
108
+ usedMargin: 700,
109
+ freeMargin: 300,
110
+ marginRatio: 30,
111
+ belowThreshold: false,
112
+ };
113
+ expect(isCriticalMargin(status)).toBe(false);
114
+ });
115
+ it("returns false at exactly 15%", () => {
116
+ const status = {
117
+ exchange: "test",
118
+ chain: "arbitrum",
119
+ equity: 1000,
120
+ usedMargin: 850,
121
+ freeMargin: 150,
122
+ marginRatio: 15,
123
+ belowThreshold: true,
124
+ };
125
+ expect(isCriticalMargin(status)).toBe(false);
126
+ });
127
+ });
128
+ // ── shouldBlockEntries ──
129
+ describe("shouldBlockEntries", () => {
130
+ it("blocks when below threshold", () => {
131
+ const status = {
132
+ exchange: "test",
133
+ chain: "solana",
134
+ equity: 1000,
135
+ usedMargin: 800,
136
+ freeMargin: 200,
137
+ marginRatio: 20,
138
+ belowThreshold: true,
139
+ };
140
+ expect(shouldBlockEntries(status, 30)).toBe(true);
141
+ });
142
+ it("allows when above threshold", () => {
143
+ const status = {
144
+ exchange: "test",
145
+ chain: "solana",
146
+ equity: 1000,
147
+ usedMargin: 500,
148
+ freeMargin: 500,
149
+ marginRatio: 50,
150
+ belowThreshold: false,
151
+ };
152
+ expect(shouldBlockEntries(status, 30)).toBe(false);
153
+ });
154
+ });
155
+ // ── computeAutoSize ──
156
+ describe("computeAutoSize", () => {
157
+ it("picks the smaller side from orderbook depth", async () => {
158
+ // Long side (asks): 3 levels -> $100*10 + $101*5 + $102*3 = $1000+$505+$306 = $1811
159
+ // Short side (bids): 3 levels -> $99*8 + $98*6 + $97*4 = $792+$588+$388 = $1768
160
+ // Min is $1768 (short side)
161
+ const longAdapter = mockAdapter({
162
+ name: "hyperliquid",
163
+ equity: 10000,
164
+ marginUsed: 0,
165
+ asks: [["100", "10"], ["101", "5"], ["102", "3"]],
166
+ });
167
+ const shortAdapter = mockAdapter({
168
+ name: "pacifica",
169
+ equity: 10000,
170
+ marginUsed: 0,
171
+ bids: [["99", "8"], ["98", "6"], ["97", "4"]],
172
+ });
173
+ const size = await computeAutoSize(longAdapter, shortAdapter, "BTC", 5.0);
174
+ // Should be capped by risk maxPositionUsd (5000 default) or the orderbook depth
175
+ expect(size).toBeGreaterThan(0);
176
+ expect(size).toBeLessThanOrEqual(5000); // risk limit
177
+ });
178
+ it("respects 50% free margin cap", async () => {
179
+ // Both sides have huge depth but small free margin
180
+ const longAdapter = mockAdapter({
181
+ name: "hyperliquid",
182
+ equity: 200,
183
+ marginUsed: 100,
184
+ asks: [["100", "100"]], // $10000 depth
185
+ });
186
+ const shortAdapter = mockAdapter({
187
+ name: "pacifica",
188
+ equity: 300,
189
+ marginUsed: 100,
190
+ bids: [["99", "100"]], // $9900 depth
191
+ });
192
+ const size = await computeAutoSize(longAdapter, shortAdapter, "BTC", 5.0);
193
+ // Free margin: long=100, short=200 -> min=100 -> 50% = 50
194
+ expect(size).toBeLessThanOrEqual(50);
195
+ expect(size).toBeGreaterThan(0);
196
+ });
197
+ it("returns 0 when orderbook is empty", async () => {
198
+ const longAdapter = mockAdapter({
199
+ name: "hyperliquid",
200
+ equity: 10000,
201
+ marginUsed: 0,
202
+ asks: [],
203
+ });
204
+ const shortAdapter = mockAdapter({
205
+ name: "pacifica",
206
+ equity: 10000,
207
+ marginUsed: 0,
208
+ bids: [["99", "10"]],
209
+ });
210
+ const size = await computeAutoSize(longAdapter, shortAdapter, "BTC", 0.3);
211
+ expect(size).toBe(0);
212
+ });
213
+ it("caps at maxPositionUsd from risk config", async () => {
214
+ // Huge depth and margin, should be capped by risk limits
215
+ const longAdapter = mockAdapter({
216
+ name: "hyperliquid",
217
+ equity: 1_000_000,
218
+ marginUsed: 0,
219
+ asks: [["100", "10000"]], // $1M depth
220
+ });
221
+ const shortAdapter = mockAdapter({
222
+ name: "pacifica",
223
+ equity: 1_000_000,
224
+ marginUsed: 0,
225
+ bids: [["99", "10000"]], // $990K depth
226
+ });
227
+ const size = await computeAutoSize(longAdapter, shortAdapter, "BTC", 5.0);
228
+ // Default maxPositionUsd is 5000
229
+ expect(size).toBeLessThanOrEqual(5000);
230
+ });
231
+ });
232
+ // ── Rebalance computation ──
233
+ describe("Rebalance computation", () => {
234
+ it("computes correct transfers for 50:50 target", async () => {
235
+ // Import the compute function
236
+ const { computeRebalancePlan } = await import("../rebalance.js");
237
+ const snapshots = [
238
+ { exchange: "lighter", equity: 800, available: 700, marginUsed: 100, unrealizedPnl: 0 },
239
+ { exchange: "pacifica", equity: 200, available: 200, marginUsed: 0, unrealizedPnl: 0 },
240
+ ];
241
+ const plan = computeRebalancePlan(snapshots, {
242
+ weights: { lighter: 0.5, pacifica: 0.5 },
243
+ minMove: 10,
244
+ reserve: 10,
245
+ });
246
+ // Total available = 900, each should have 450
247
+ // Lighter has 700 (surplus ~250), Pacifica has 200 (deficit ~250)
248
+ expect(plan.moves.length).toBeGreaterThan(0);
249
+ // The move should be from lighter to pacifica
250
+ const move = plan.moves[0];
251
+ expect(move.from).toBe("lighter");
252
+ expect(move.to).toBe("pacifica");
253
+ expect(move.amount).toBeGreaterThan(100); // Should move ~230+ (minus reserve)
254
+ });
255
+ it("returns no moves when already balanced", async () => {
256
+ const { computeRebalancePlan } = await import("../rebalance.js");
257
+ const snapshots = [
258
+ { exchange: "lighter", equity: 500, available: 500, marginUsed: 0, unrealizedPnl: 0 },
259
+ { exchange: "pacifica", equity: 500, available: 500, marginUsed: 0, unrealizedPnl: 0 },
260
+ ];
261
+ const plan = computeRebalancePlan(snapshots, {
262
+ weights: { lighter: 0.5, pacifica: 0.5 },
263
+ minMove: 10,
264
+ reserve: 10,
265
+ });
266
+ expect(plan.moves.length).toBe(0);
267
+ });
268
+ it("respects 33:33:33 three-way split", async () => {
269
+ const { computeRebalancePlan } = await import("../rebalance.js");
270
+ const snapshots = [
271
+ { exchange: "lighter", equity: 900, available: 900, marginUsed: 0, unrealizedPnl: 0 },
272
+ { exchange: "pacifica", equity: 0, available: 0, marginUsed: 0, unrealizedPnl: 0 },
273
+ { exchange: "hyperliquid", equity: 0, available: 0, marginUsed: 0, unrealizedPnl: 0 },
274
+ ];
275
+ const plan = computeRebalancePlan(snapshots, {
276
+ weights: { lighter: 1 / 3, pacifica: 1 / 3, hyperliquid: 1 / 3 },
277
+ minMove: 10,
278
+ reserve: 10,
279
+ });
280
+ // Should move funds from lighter to the other two
281
+ expect(plan.moves.length).toBeGreaterThanOrEqual(1);
282
+ for (const m of plan.moves) {
283
+ expect(m.from).toBe("lighter");
284
+ expect(["pacifica", "hyperliquid"]).toContain(m.to);
285
+ }
286
+ });
287
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,191 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { findDexArbPairs } from "../dex-asset-map.js";
3
+ function makeAsset(overrides) {
4
+ return {
5
+ maxLeverage: 10,
6
+ openInterest: 1000,
7
+ volume24h: 50000,
8
+ szDecimals: 3,
9
+ ...overrides,
10
+ };
11
+ }
12
+ describe("findDexArbPairs — exact name matching", () => {
13
+ it("finds arb pair for TSLA across xyz and cash dexes", () => {
14
+ const assets = [
15
+ makeAsset({ raw: "xyz:TSLA", base: "TSLA", dex: "xyz", markPrice: 392.81, fundingRate: 0.00001 }),
16
+ makeAsset({ raw: "cash:TSLA", base: "TSLA", dex: "cash", markPrice: 392.65, fundingRate: -0.00005 }),
17
+ ];
18
+ const pairs = findDexArbPairs(assets);
19
+ expect(pairs).toHaveLength(1);
20
+ expect(pairs[0].underlying).toBe("TSLA");
21
+ expect(pairs[0].long.dex).not.toBe(pairs[0].short.dex);
22
+ expect(pairs[0].annualSpread).toBeGreaterThan(0);
23
+ expect(pairs[0].priceGapPct).toBeLessThan(1);
24
+ });
25
+ it("finds multiple pairs for NVDA across 4 dexes", () => {
26
+ const assets = [
27
+ makeAsset({ raw: "xyz:NVDA", base: "NVDA", dex: "xyz", markPrice: 175.97, fundingRate: 0.00004 }),
28
+ makeAsset({ raw: "flx:NVDA", base: "NVDA", dex: "flx", markPrice: 176.02, fundingRate: 0.0 }),
29
+ makeAsset({ raw: "km:NVDA", base: "NVDA", dex: "km", markPrice: 176.01, fundingRate: -0.00006 }),
30
+ makeAsset({ raw: "cash:NVDA", base: "NVDA", dex: "cash", markPrice: 176.19, fundingRate: -0.000001 }),
31
+ ];
32
+ const pairs = findDexArbPairs(assets);
33
+ // 4 dexes → C(4,2) = 6 pairs
34
+ expect(pairs.length).toBe(6);
35
+ // All should be NVDA
36
+ for (const p of pairs) {
37
+ expect(p.underlying).toBe("NVDA");
38
+ }
39
+ // Sorted by spread descending
40
+ for (let i = 1; i < pairs.length; i++) {
41
+ expect(pairs[i].annualSpread).toBeLessThanOrEqual(pairs[i - 1].annualSpread);
42
+ }
43
+ });
44
+ it("includes native HL perps when includeNative=true", () => {
45
+ const assets = [
46
+ makeAsset({ raw: "BTC", base: "BTC", dex: "hl", markPrice: 68000, fundingRate: 0.00001 }),
47
+ makeAsset({ raw: "hyna:BTC", base: "BTC", dex: "hyna", markPrice: 68010, fundingRate: 0.00003 }),
48
+ ];
49
+ const withNative = findDexArbPairs(assets, { includeNative: true });
50
+ expect(withNative).toHaveLength(1);
51
+ expect(withNative[0].underlying).toBe("BTC");
52
+ const withoutNative = findDexArbPairs(assets, { includeNative: false });
53
+ expect(withoutNative).toHaveLength(0); // only 1 non-native asset
54
+ });
55
+ });
56
+ describe("findDexArbPairs — alias matching", () => {
57
+ it("matches CL and OIL as same underlying (CRUDE_OIL_WTI)", () => {
58
+ const assets = [
59
+ makeAsset({ raw: "xyz:CL", base: "CL", dex: "xyz", markPrice: 93.34, fundingRate: -0.0005 }),
60
+ makeAsset({ raw: "flx:OIL", base: "OIL", dex: "flx", markPrice: 93.38, fundingRate: 0.0 }),
61
+ ];
62
+ const pairs = findDexArbPairs(assets);
63
+ expect(pairs).toHaveLength(1);
64
+ expect(pairs[0].underlying).toBe("CRUDE_OIL_WTI");
65
+ });
66
+ it("matches kPEPE and 1000PEPE as same underlying", () => {
67
+ const assets = [
68
+ makeAsset({ raw: "kPEPE", base: "kPEPE", dex: "hl", markPrice: 0.015, fundingRate: 0.0001 }),
69
+ makeAsset({ raw: "hyna:1000PEPE", base: "1000PEPE", dex: "hyna", markPrice: 0.0151, fundingRate: 0.0003 }),
70
+ ];
71
+ const pairs = findDexArbPairs(assets);
72
+ expect(pairs).toHaveLength(1);
73
+ expect(pairs[0].underlying).toBe("1000PEPE");
74
+ });
75
+ });
76
+ describe("findDexArbPairs — blacklist & price gap filtering", () => {
77
+ it("rejects USAR vs US500 (blacklisted)", () => {
78
+ const assets = [
79
+ makeAsset({ raw: "xyz:USAR", base: "USAR", dex: "xyz", markPrice: 17.63, fundingRate: 0.0003 }),
80
+ makeAsset({ raw: "km:US500", base: "US500", dex: "km", markPrice: 666.69, fundingRate: 0.00001 }),
81
+ ];
82
+ // Even without blacklist, price gap (>5%) would filter this out
83
+ const pairs = findDexArbPairs(assets);
84
+ expect(pairs).toHaveLength(0);
85
+ });
86
+ it("rejects SEMI vs SEMIS (blacklisted)", () => {
87
+ const assets = [
88
+ makeAsset({ raw: "km:SEMI", base: "SEMI", dex: "km", markPrice: 319.38, fundingRate: 0.00008 }),
89
+ makeAsset({ raw: "vntl:SEMIS", base: "SEMIS", dex: "vntl", markPrice: 381.21, fundingRate: 0.00001 }),
90
+ ];
91
+ const pairs = findDexArbPairs(assets);
92
+ expect(pairs).toHaveLength(0);
93
+ });
94
+ it("rejects pairs with >5% price gap even if same name", () => {
95
+ const assets = [
96
+ makeAsset({ raw: "dexA:FOO", base: "FOO", dex: "dexA", markPrice: 100, fundingRate: 0.001 }),
97
+ makeAsset({ raw: "dexB:FOO", base: "FOO", dex: "dexB", markPrice: 110, fundingRate: -0.001 }),
98
+ ];
99
+ // ~9.5% gap → should be filtered
100
+ const pairs = findDexArbPairs(assets, { maxPriceGapPct: 5 });
101
+ expect(pairs).toHaveLength(0);
102
+ // Increase tolerance
103
+ const pairsLoose = findDexArbPairs(assets, { maxPriceGapPct: 15 });
104
+ expect(pairsLoose).toHaveLength(1);
105
+ });
106
+ it("accepts pairs with <5% price gap", () => {
107
+ const assets = [
108
+ makeAsset({ raw: "xyz:GOLD", base: "GOLD", dex: "xyz", markPrice: 5164.60, fundingRate: -0.00001 }),
109
+ makeAsset({ raw: "cash:GOLD", base: "GOLD", dex: "cash", markPrice: 5176.04, fundingRate: -0.00005 }),
110
+ ];
111
+ const pairs = findDexArbPairs(assets);
112
+ expect(pairs).toHaveLength(1);
113
+ expect(pairs[0].priceGapPct).toBeLessThan(1);
114
+ });
115
+ });
116
+ describe("findDexArbPairs — spread calculation", () => {
117
+ it("correctly determines long/short direction", () => {
118
+ const assets = [
119
+ // dexA: high positive funding → expensive to be long
120
+ makeAsset({ raw: "dexA:ETH", base: "ETH", dex: "dexA", markPrice: 1974, fundingRate: 0.001 }),
121
+ // dexB: negative funding → get paid to be long
122
+ makeAsset({ raw: "dexB:ETH", base: "ETH", dex: "dexB", markPrice: 1975, fundingRate: -0.001 }),
123
+ ];
124
+ const pairs = findDexArbPairs(assets);
125
+ expect(pairs).toHaveLength(1);
126
+ // Should long on dexB (lower funding) and short on dexA (higher funding)
127
+ expect(pairs[0].long.dex).toBe("dexB");
128
+ expect(pairs[0].short.dex).toBe("dexA");
129
+ });
130
+ it("all dexes use 1h funding period (including HIP-3 deployed)", () => {
131
+ const assets = [
132
+ // Native HL: 1h funding
133
+ makeAsset({ raw: "BTC", base: "BTC", dex: "hl", markPrice: 68000, fundingRate: 0.001 }),
134
+ // Deployed dex: also 1h funding (same rate = no spread)
135
+ makeAsset({ raw: "hyna:BTC", base: "BTC", dex: "hyna", markPrice: 68010, fundingRate: 0.001 }),
136
+ ];
137
+ const pairs = findDexArbPairs(assets, { minAnnualSpread: 0 });
138
+ // Same rate on both → spread ≈ 0
139
+ if (pairs.length > 0) {
140
+ expect(pairs[0].annualSpread).toBeLessThan(1);
141
+ }
142
+ });
143
+ it("filters by minAnnualSpread", () => {
144
+ const assets = [
145
+ makeAsset({ raw: "xyz:TSLA", base: "TSLA", dex: "xyz", markPrice: 392, fundingRate: 0.00001 }),
146
+ makeAsset({ raw: "cash:TSLA", base: "TSLA", dex: "cash", markPrice: 393, fundingRate: 0.00002 }),
147
+ ];
148
+ const allPairs = findDexArbPairs(assets, { minAnnualSpread: 0 });
149
+ expect(allPairs.length).toBeGreaterThanOrEqual(1);
150
+ const highOnly = findDexArbPairs(assets, { minAnnualSpread: 999 });
151
+ expect(highOnly).toHaveLength(0);
152
+ });
153
+ });
154
+ describe("findDexArbPairs — same dex exclusion", () => {
155
+ it("does not pair assets from the same dex", () => {
156
+ const assets = [
157
+ makeAsset({ raw: "xyz:TSLA", base: "TSLA", dex: "xyz", markPrice: 392, fundingRate: 0.001 }),
158
+ makeAsset({ raw: "xyz:NVDA", base: "NVDA", dex: "xyz", markPrice: 176, fundingRate: -0.001 }),
159
+ ];
160
+ const pairs = findDexArbPairs(assets);
161
+ expect(pairs).toHaveLength(0); // different assets, no match
162
+ });
163
+ it("does not pair same asset within same dex", () => {
164
+ // Edge case: shouldn't happen but be safe
165
+ const assets = [
166
+ makeAsset({ raw: "xyz:TSLA", base: "TSLA", dex: "xyz", markPrice: 392, fundingRate: 0.001 }),
167
+ makeAsset({ raw: "xyz:TSLA", base: "TSLA", dex: "xyz", markPrice: 392, fundingRate: 0.001 }),
168
+ ];
169
+ const pairs = findDexArbPairs(assets);
170
+ expect(pairs).toHaveLength(0);
171
+ });
172
+ });
173
+ describe("findDexArbPairs — edge cases", () => {
174
+ it("handles empty assets list", () => {
175
+ expect(findDexArbPairs([])).toHaveLength(0);
176
+ });
177
+ it("handles single asset", () => {
178
+ const assets = [
179
+ makeAsset({ raw: "BTC", base: "BTC", dex: "hl", markPrice: 68000, fundingRate: 0.001 }),
180
+ ];
181
+ expect(findDexArbPairs(assets)).toHaveLength(0);
182
+ });
183
+ it("handles all assets from one dex only", () => {
184
+ const assets = [
185
+ makeAsset({ raw: "xyz:TSLA", base: "TSLA", dex: "xyz", markPrice: 392, fundingRate: 0.001 }),
186
+ makeAsset({ raw: "xyz:NVDA", base: "NVDA", dex: "xyz", markPrice: 176, fundingRate: -0.001 }),
187
+ makeAsset({ raw: "xyz:GOLD", base: "GOLD", dex: "xyz", markPrice: 5164, fundingRate: 0.0 }),
188
+ ];
189
+ expect(findDexArbPairs(assets)).toHaveLength(0);
190
+ });
191
+ });
@@ -0,0 +1 @@
1
+ export {};