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,397 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { computeNetSpread, computeRoundTripCostPct, getNextSettlement, isNearSettlement, isSpreadReversed, } from "../commands/arb-auto.js";
3
+ /**
4
+ * Mirrors the direction logic in arb-auto.ts:
5
+ * Sort all available exchange rates by hourly-normalized rate.
6
+ * Long on the lowest-rate exchange, short on the highest.
7
+ */
8
+ function determine3DexDirection(snap) {
9
+ return {
10
+ longExchange: snap.longExch,
11
+ shortExchange: snap.shortExch,
12
+ };
13
+ }
14
+ /**
15
+ * Mirrors the funding accumulation logic in arb-auto:
16
+ * Estimate funding collected based on rate differential and elapsed time.
17
+ */
18
+ function accumulateFunding(pos, current, nowMs) {
19
+ const elapsedHours = (nowMs - pos.lastCheckTime) / (1000 * 60 * 60);
20
+ const notional = parseFloat(pos.size) * current.markPrice;
21
+ const rateFor = (e) => e === "pacifica" ? current.pacRate : e === "hyperliquid" ? current.hlRate : current.ltRate;
22
+ const longHourly = rateFor(pos.longExchange) / 1; // all exchanges are hourly
23
+ const shortHourly = rateFor(pos.shortExchange) / 1; // all exchanges are hourly
24
+ const hourlyIncome = (shortHourly - longHourly) * notional;
25
+ return hourlyIncome * elapsedHours;
26
+ }
27
+ // ──────────────────────────────────────────────
28
+ // 3-DEX direction
29
+ // ──────────────────────────────────────────────
30
+ describe("3-DEX direction determination", () => {
31
+ it("uses longExch/shortExch from snapshot (all 3 available)", () => {
32
+ const snap = {
33
+ symbol: "BTC",
34
+ pacRate: 0.0006, // pac = 0.0006/hr
35
+ hlRate: 0.0003, // hl = 0.0003/hr
36
+ ltRate: 0.0002, // lt = 0.0002/hr (lowest!)
37
+ spread: 240.9,
38
+ longExch: "lighter",
39
+ shortExch: "pacifica",
40
+ markPrice: 60000,
41
+ pacMarkPrice: 0, hlMarkPrice: 0, ltMarkPrice: 0,
42
+ };
43
+ const { longExchange, shortExchange } = determine3DexDirection(snap);
44
+ expect(longExchange).toBe("lighter");
45
+ expect(shortExchange).toBe("pacifica");
46
+ });
47
+ it("handles pac vs hl only (lighter missing)", () => {
48
+ const snap = {
49
+ symbol: "ETH",
50
+ pacRate: 0.002, // pac = 0.002/hr (highest)
51
+ hlRate: 0.00005, // hl = 0.00005/hr (lowest)
52
+ ltRate: 0, // no lighter
53
+ spread: 175.2,
54
+ longExch: "hyperliquid",
55
+ shortExch: "pacifica",
56
+ markPrice: 3000,
57
+ pacMarkPrice: 0, hlMarkPrice: 0, ltMarkPrice: 0,
58
+ };
59
+ const { longExchange, shortExchange } = determine3DexDirection(snap);
60
+ expect(longExchange).toBe("hyperliquid");
61
+ expect(shortExchange).toBe("pacifica");
62
+ });
63
+ it("handles lighter vs pac when HL missing", () => {
64
+ const snap = {
65
+ symbol: "SOL",
66
+ pacRate: 0.0001,
67
+ hlRate: 0,
68
+ ltRate: 0.001,
69
+ spread: 98.55,
70
+ longExch: "pacifica",
71
+ shortExch: "lighter",
72
+ markPrice: 150,
73
+ pacMarkPrice: 0, hlMarkPrice: 0, ltMarkPrice: 0,
74
+ };
75
+ const { longExchange, shortExchange } = determine3DexDirection(snap);
76
+ expect(longExchange).toBe("pacifica");
77
+ expect(shortExchange).toBe("lighter");
78
+ });
79
+ });
80
+ // ──────────────────────────────────────────────
81
+ // Funding accumulation
82
+ // ──────────────────────────────────────────────
83
+ describe("Funding accumulation tracking", () => {
84
+ const baseTime = Date.now();
85
+ it("accumulates positive income when spread is favorable", () => {
86
+ const pos = {
87
+ symbol: "BTC",
88
+ longExchange: "hyperliquid", // low funding
89
+ shortExchange: "pacifica", // high funding
90
+ size: "0.1",
91
+ entrySpread: 50,
92
+ entryTime: new Date().toISOString(),
93
+ entryMarkPrice: 60000,
94
+ accumulatedFundingUsd: 0,
95
+ lastCheckTime: baseTime,
96
+ };
97
+ const snap = {
98
+ symbol: "BTC",
99
+ pacRate: 0.000125, // pac = 0.000125/hr (all hourly now)
100
+ hlRate: 0.00005, // hl = 0.00005/hr
101
+ ltRate: 0,
102
+ spread: 65.7,
103
+ longExch: "hyperliquid",
104
+ shortExch: "pacifica",
105
+ markPrice: 60000,
106
+ pacMarkPrice: 0, hlMarkPrice: 0, ltMarkPrice: 0,
107
+ };
108
+ // 1 hour elapsed
109
+ const income = accumulateFunding(pos, snap, baseTime + 3600_000);
110
+ // shortHourly = 0.000125
111
+ // longHourly = 0.00005
112
+ // diff = 0.000125 - 0.00005 = 0.000075
113
+ // notional = 0.1 * 60000 = 6000
114
+ // income = 0.000075 * 6000 * 1 = $0.45/hr
115
+ expect(income).toBeCloseTo(0.45, 2);
116
+ });
117
+ it("returns zero income when no time elapsed", () => {
118
+ const pos = {
119
+ symbol: "ETH",
120
+ longExchange: "lighter",
121
+ shortExchange: "pacifica",
122
+ size: "1",
123
+ entrySpread: 30,
124
+ entryTime: new Date().toISOString(),
125
+ entryMarkPrice: 3000,
126
+ accumulatedFundingUsd: 0,
127
+ lastCheckTime: baseTime,
128
+ };
129
+ const snap = {
130
+ symbol: "ETH",
131
+ pacRate: 0.0005, // all hourly now
132
+ hlRate: 0.0001,
133
+ ltRate: 0.0001,
134
+ spread: 43.8,
135
+ longExch: "lighter",
136
+ shortExch: "pacifica",
137
+ markPrice: 3000,
138
+ pacMarkPrice: 0, hlMarkPrice: 0, ltMarkPrice: 0,
139
+ };
140
+ const income = accumulateFunding(pos, snap, baseTime); // same time
141
+ expect(income).toBe(0);
142
+ });
143
+ it("scales with elapsed time", () => {
144
+ const pos = {
145
+ symbol: "BTC",
146
+ longExchange: "hyperliquid",
147
+ shortExchange: "pacifica",
148
+ size: "0.1",
149
+ entrySpread: 50,
150
+ entryTime: new Date().toISOString(),
151
+ entryMarkPrice: 60000,
152
+ accumulatedFundingUsd: 0,
153
+ lastCheckTime: baseTime,
154
+ };
155
+ const snap = {
156
+ symbol: "BTC",
157
+ pacRate: 0.000125,
158
+ hlRate: 0.00005,
159
+ ltRate: 0,
160
+ spread: 65.7,
161
+ longExch: "hyperliquid",
162
+ shortExch: "pacifica",
163
+ markPrice: 60000,
164
+ pacMarkPrice: 0, hlMarkPrice: 0, ltMarkPrice: 0,
165
+ };
166
+ const income1h = accumulateFunding(pos, snap, baseTime + 3600_000);
167
+ const income24h = accumulateFunding(pos, snap, baseTime + 24 * 3600_000);
168
+ expect(income24h).toBeCloseTo(income1h * 24, 2);
169
+ });
170
+ it("can produce negative income if spread reverses", () => {
171
+ const pos = {
172
+ symbol: "BTC",
173
+ longExchange: "pacifica", // originally was low
174
+ shortExchange: "hyperliquid", // originally was high
175
+ size: "0.1",
176
+ entrySpread: 20,
177
+ entryTime: new Date().toISOString(),
178
+ entryMarkPrice: 60000,
179
+ accumulatedFundingUsd: 0,
180
+ lastCheckTime: baseTime,
181
+ };
182
+ // Now PAC rate is HIGHER than HL — bad for our direction
183
+ const snap = {
184
+ symbol: "BTC",
185
+ pacRate: 0.00025, // pac now high: 0.00025/hr
186
+ hlRate: 0.00001, // hl now low: 0.00001/hr
187
+ ltRate: 0,
188
+ spread: 218.3,
189
+ longExch: "hyperliquid",
190
+ shortExch: "pacifica",
191
+ markPrice: 60000,
192
+ pacMarkPrice: 0, hlMarkPrice: 0, ltMarkPrice: 0,
193
+ };
194
+ const income = accumulateFunding(pos, snap, baseTime + 3600_000);
195
+ // shortHourly (HL) = 0.00001
196
+ // longHourly (PAC) = 0.00025
197
+ // diff = 0.00001 - 0.00025 = -0.00024
198
+ // notional = 6000
199
+ // income = -0.00024 * 6000 * 1 = -$1.44
200
+ expect(income).toBeLessThan(0);
201
+ });
202
+ });
203
+ // ──────────────────────────────────────────────
204
+ // Entry/exit conditions with 3 DEXs
205
+ // ──────────────────────────────────────────────
206
+ describe("3-DEX entry/exit conditions", () => {
207
+ const minSpread = 30;
208
+ const closeSpread = 5;
209
+ it("enters when any 2-exchange spread exceeds threshold", () => {
210
+ // Only PAC and LT available, but spread is large
211
+ const absSpread = 45;
212
+ expect(absSpread >= minSpread).toBe(true);
213
+ });
214
+ it("closes when best available spread drops below close threshold", () => {
215
+ const currentSpread = 3;
216
+ expect(currentSpread <= closeSpread).toBe(true);
217
+ });
218
+ it("does not enter if max positions reached", () => {
219
+ const maxPositions = 3;
220
+ const openPositions = 3;
221
+ expect(openPositions >= maxPositions).toBe(true);
222
+ });
223
+ it("skips symbol already in open positions", () => {
224
+ const openPositions = [{
225
+ symbol: "BTC",
226
+ longExchange: "hyperliquid",
227
+ shortExchange: "pacifica",
228
+ size: "0.1",
229
+ entrySpread: 40,
230
+ entryTime: new Date().toISOString(),
231
+ entryMarkPrice: 60000,
232
+ accumulatedFundingUsd: 0.5,
233
+ lastCheckTime: Date.now(),
234
+ }];
235
+ const alreadyOpen = openPositions.some(p => p.symbol === "BTC");
236
+ expect(alreadyOpen).toBe(true);
237
+ });
238
+ });
239
+ // ──────────────────────────────────────────────
240
+ // Net spread calculation
241
+ // ──────────────────────────────────────────────
242
+ describe("computeNetSpread", () => {
243
+ it("correctly deducts annualized round-trip cost from gross spread", () => {
244
+ // gross=30%, hold=7d, roundTrip=0.14% → net = 30 - (0.14/7*365) = 30 - 7.3 = 22.7
245
+ const net = computeNetSpread(30, 7, 0.14);
246
+ expect(net).toBeCloseTo(22.7, 1);
247
+ });
248
+ it("returns gross spread when costs are zero", () => {
249
+ const net = computeNetSpread(50, 7, 0);
250
+ expect(net).toBe(50);
251
+ });
252
+ it("can produce negative net spread when costs exceed gross", () => {
253
+ // gross=5%, hold=1d, roundTrip=0.14% → net = 5 - (0.14*365) = 5 - 51.1 = -46.1
254
+ const net = computeNetSpread(5, 1, 0.14);
255
+ expect(net).toBeLessThan(0);
256
+ });
257
+ it("longer hold periods reduce annualized cost impact", () => {
258
+ const net7d = computeNetSpread(30, 7, 0.14);
259
+ const net30d = computeNetSpread(30, 30, 0.14);
260
+ expect(net30d).toBeGreaterThan(net7d);
261
+ });
262
+ it("includes bridge cost in net spread calculation", () => {
263
+ // Without bridge cost
264
+ const netNoBridge = computeNetSpread(30, 7, 0.14, 0, 100);
265
+ // With $0.50 bridge cost, $100 position
266
+ // bridgeRoundTripPct = (0.5 * 2 / 100) * 100 = 1%
267
+ // bridgeAnnualized = (1/7) * 365 = 52.14%
268
+ const netWithBridge = computeNetSpread(30, 7, 0.14, 0.5, 100);
269
+ expect(netWithBridge).toBeLessThan(netNoBridge);
270
+ // Difference should be the annualized bridge cost
271
+ const bridgeDiff = netNoBridge - netWithBridge;
272
+ expect(bridgeDiff).toBeCloseTo(52.14, 0);
273
+ });
274
+ it("bridge cost impact scales inversely with position size", () => {
275
+ const netSmall = computeNetSpread(30, 7, 0.14, 0.5, 50); // $50 position
276
+ const netLarge = computeNetSpread(30, 7, 0.14, 0.5, 500); // $500 position
277
+ // Larger positions dilute bridge cost
278
+ expect(netLarge).toBeGreaterThan(netSmall);
279
+ });
280
+ });
281
+ describe("computeRoundTripCostPct", () => {
282
+ it("computes round-trip cost for same-fee exchanges", () => {
283
+ // 2 × (0.035% + 0.035%) + 2 × 0.05% = 0.24%
284
+ const cost = computeRoundTripCostPct("hyperliquid", "pacifica", 0.05);
285
+ expect(cost).toBeCloseTo(0.24, 4);
286
+ });
287
+ it("uses default slippage of 0.05%", () => {
288
+ const cost = computeRoundTripCostPct("hyperliquid", "lighter");
289
+ // 2 × (0.035% + 0.035%) + 2 × 0.05% = 0.24%
290
+ expect(cost).toBeCloseTo(0.24, 4);
291
+ });
292
+ it("handles custom slippage", () => {
293
+ const cost = computeRoundTripCostPct("hyperliquid", "pacifica", 0.1);
294
+ // 2 × (0.035% + 0.035%) + 2 × 0.1% = 0.34%
295
+ expect(cost).toBeCloseTo(0.34, 4);
296
+ });
297
+ });
298
+ // ──────────────────────────────────────────────
299
+ // Spread reversal detection
300
+ // ──────────────────────────────────────────────
301
+ describe("Spread reversal detection", () => {
302
+ it("detects reversal when long exchange rate exceeds short", () => {
303
+ const snap = {
304
+ symbol: "BTC",
305
+ pacRate: 0.0001, // PAC low (was short, now low)
306
+ hlRate: 0.0005, // HL high (was long, now high)
307
+ ltRate: 0,
308
+ spread: 30,
309
+ longExch: "pacifica",
310
+ shortExch: "hyperliquid",
311
+ markPrice: 60000,
312
+ pacMarkPrice: 0, hlMarkPrice: 0, ltMarkPrice: 0,
313
+ };
314
+ // Position: long HL, short PAC — but now HL hourly (0.0005) > PAC hourly (0.0001)
315
+ const reversed = isSpreadReversed("hyperliquid", "pacifica", snap);
316
+ expect(reversed).toBe(true);
317
+ });
318
+ it("does not flag reversal when spread is still favorable", () => {
319
+ const snap = {
320
+ symbol: "BTC",
321
+ pacRate: 0.001, // PAC high (0.001/hr)
322
+ hlRate: 0.00005, // HL low (0.00005/hr)
323
+ ltRate: 0,
324
+ spread: 65.7,
325
+ longExch: "hyperliquid",
326
+ shortExch: "pacifica",
327
+ markPrice: 60000,
328
+ pacMarkPrice: 0, hlMarkPrice: 0, ltMarkPrice: 0,
329
+ };
330
+ // Position: long HL, short PAC — HL hourly (0.00005) < PAC hourly (0.001) → no reversal
331
+ const reversed = isSpreadReversed("hyperliquid", "pacifica", snap);
332
+ expect(reversed).toBe(false);
333
+ });
334
+ it("handles lighter vs pacifica reversal", () => {
335
+ const snap = {
336
+ symbol: "SOL",
337
+ pacRate: 0.0001, // PAC hourly = 0.0001
338
+ hlRate: 0,
339
+ ltRate: 0.0016, // LT 8h rate, hourly = 0.0016/8 = 0.0002
340
+ spread: 10,
341
+ longExch: "pacifica",
342
+ shortExch: "lighter",
343
+ markPrice: 150,
344
+ pacMarkPrice: 0, hlMarkPrice: 0, ltMarkPrice: 0,
345
+ };
346
+ // Position: long LT, short PAC — LT hourly (0.0002) > PAC hourly (0.0001) → reversed
347
+ const reversed = isSpreadReversed("lighter", "pacifica", snap);
348
+ expect(reversed).toBe(true);
349
+ });
350
+ });
351
+ // ──────────────────────────────────────────────
352
+ // Settlement timing awareness
353
+ // ──────────────────────────────────────────────
354
+ describe("Settlement timing awareness", () => {
355
+ it("getNextSettlement returns next hour for HL", () => {
356
+ // At 14:30 UTC, next HL settlement is at 15:00 UTC
357
+ const now = new Date("2025-01-15T14:30:00Z");
358
+ const next = getNextSettlement("hyperliquid", now);
359
+ expect(next.getUTCHours()).toBe(15);
360
+ expect(next.getUTCMinutes()).toBe(0);
361
+ });
362
+ it("getNextSettlement returns next hour for PAC (hourly like HL)", () => {
363
+ // At 06:00 UTC, next PAC settlement is at 07:00 UTC
364
+ const now = new Date("2025-01-15T06:30:00Z");
365
+ const next = getNextSettlement("pacifica", now);
366
+ expect(next.getUTCHours()).toBe(7);
367
+ });
368
+ it("getNextSettlement wraps to next day for PAC when at 23:xx", () => {
369
+ // At 23:30 UTC, next PAC settlement is at 00:00 next day
370
+ const now = new Date("2025-01-15T23:30:00Z");
371
+ const next = getNextSettlement("pacifica", now);
372
+ expect(next.getUTCHours()).toBe(0);
373
+ expect(next.getUTCDate()).toBe(16);
374
+ });
375
+ it("isNearSettlement blocks entry within 5 minutes of settlement", () => {
376
+ // 3 minutes before 08:00 UTC settlement — all exchanges settle hourly now
377
+ const now = new Date("2025-01-15T07:57:00Z");
378
+ const result = isNearSettlement("lighter", "pacifica", 5, now);
379
+ expect(result.blocked).toBe(true);
380
+ // Both lighter and pacifica settle at 08:00, lighter checked first
381
+ expect(result.exchange).toBe("lighter");
382
+ expect(result.minutesUntil).toBeLessThanOrEqual(5);
383
+ });
384
+ it("isNearSettlement allows entry far from settlement", () => {
385
+ // 30 minutes past the hour — next settlement is 30 min away
386
+ const now = new Date("2025-01-15T15:30:00Z");
387
+ const result = isNearSettlement("lighter", "pacifica", 5, now);
388
+ expect(result.blocked).toBe(false);
389
+ });
390
+ it("isNearSettlement checks both exchanges", () => {
391
+ // 2 minutes before hourly HL settlement
392
+ const now = new Date("2025-01-15T14:58:00Z");
393
+ const result = isNearSettlement("hyperliquid", "pacifica", 5, now);
394
+ expect(result.blocked).toBe(true);
395
+ expect(result.exchange).toBe("hyperliquid");
396
+ });
397
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,176 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { computeEnhancedStats, normalizeExchangePair, getTimeBucket, } from "../arb-history-stats.js";
3
+ function makeTrade(overrides = {}) {
4
+ return {
5
+ symbol: "ETH",
6
+ exchanges: "hyperliquid+pacifica",
7
+ entryDate: "2025-01-15T10:00:00.000Z",
8
+ exitDate: "2025-01-19T14:00:00.000Z",
9
+ holdDurationMs: 4 * 24 * 60 * 60 * 1000 + 4 * 60 * 60 * 1000, // 4d 4h
10
+ entrySpread: 35.0,
11
+ exitSpread: 8.0,
12
+ netReturn: 5.20,
13
+ status: "completed",
14
+ ...overrides,
15
+ };
16
+ }
17
+ describe("normalizeExchangePair", () => {
18
+ it("converts exchange names to abbreviations and sorts", () => {
19
+ expect(normalizeExchangePair("hyperliquid+pacifica")).toBe("HL/PAC");
20
+ expect(normalizeExchangePair("pacifica+hyperliquid")).toBe("HL/PAC");
21
+ expect(normalizeExchangePair("lighter+hyperliquid")).toBe("HL/LT");
22
+ expect(normalizeExchangePair("lighter+pacifica")).toBe("LT/PAC");
23
+ });
24
+ it("handles unknown exchanges with truncation", () => {
25
+ expect(normalizeExchangePair("binance+coinbase")).toBe("BIN/COI");
26
+ });
27
+ });
28
+ describe("getTimeBucket", () => {
29
+ it("returns correct 4-hour UTC buckets", () => {
30
+ expect(getTimeBucket("2025-01-15T00:00:00.000Z")).toBe("00-04 UTC");
31
+ expect(getTimeBucket("2025-01-15T03:59:59.000Z")).toBe("00-04 UTC");
32
+ expect(getTimeBucket("2025-01-15T04:00:00.000Z")).toBe("04-08 UTC");
33
+ expect(getTimeBucket("2025-01-15T08:30:00.000Z")).toBe("08-12 UTC");
34
+ expect(getTimeBucket("2025-01-15T12:00:00.000Z")).toBe("12-16 UTC");
35
+ expect(getTimeBucket("2025-01-15T16:45:00.000Z")).toBe("16-20 UTC");
36
+ expect(getTimeBucket("2025-01-15T20:00:00.000Z")).toBe("20-24 UTC");
37
+ expect(getTimeBucket("2025-01-15T23:59:59.000Z")).toBe("20-24 UTC");
38
+ });
39
+ });
40
+ describe("computeEnhancedStats", () => {
41
+ it("handles empty history gracefully", () => {
42
+ const stats = computeEnhancedStats([]);
43
+ expect(stats.avgEntrySpread).toBe(0);
44
+ expect(stats.avgExitSpread).toBe(0);
45
+ expect(stats.avgSpreadDecay).toBe(0);
46
+ expect(stats.byExchangePair).toEqual([]);
47
+ expect(stats.byTimeOfDay).toEqual([]);
48
+ expect(stats.optimalHoldTime).toBeNull();
49
+ expect(stats.optimalHoldTimeMs).toBeNull();
50
+ });
51
+ it("ignores open and failed trades in completed stats", () => {
52
+ const trades = [
53
+ makeTrade({ status: "open", netReturn: 0 }),
54
+ makeTrade({ status: "failed", netReturn: -1 }),
55
+ ];
56
+ const stats = computeEnhancedStats(trades);
57
+ expect(stats.byExchangePair).toEqual([]);
58
+ expect(stats.byTimeOfDay).toEqual([]);
59
+ });
60
+ it("computes average entry/exit spreads correctly", () => {
61
+ const trades = [
62
+ makeTrade({ entrySpread: 30, exitSpread: 10 }),
63
+ makeTrade({ entrySpread: 40, exitSpread: 6 }),
64
+ ];
65
+ const stats = computeEnhancedStats(trades);
66
+ expect(stats.avgEntrySpread).toBe(35);
67
+ expect(stats.avgExitSpread).toBe(8);
68
+ expect(stats.avgSpreadDecay).toBe(27); // (20 + 34) / 2
69
+ });
70
+ it("handles null spreads in averages", () => {
71
+ const trades = [
72
+ makeTrade({ entrySpread: 30, exitSpread: null }),
73
+ makeTrade({ entrySpread: null, exitSpread: 10 }),
74
+ ];
75
+ const stats = computeEnhancedStats(trades);
76
+ expect(stats.avgEntrySpread).toBe(30);
77
+ expect(stats.avgExitSpread).toBe(10);
78
+ expect(stats.avgSpreadDecay).toBe(0); // neither has both
79
+ });
80
+ it("groups by exchange pair correctly", () => {
81
+ const trades = [
82
+ makeTrade({ exchanges: "hyperliquid+pacifica", netReturn: 10 }),
83
+ makeTrade({ exchanges: "hyperliquid+pacifica", netReturn: 5 }),
84
+ makeTrade({ exchanges: "hyperliquid+pacifica", netReturn: -2 }),
85
+ makeTrade({ exchanges: "lighter+pacifica", netReturn: 8 }),
86
+ makeTrade({ exchanges: "lighter+pacifica", netReturn: 3 }),
87
+ makeTrade({ exchanges: "hyperliquid+lighter", netReturn: 1 }),
88
+ ];
89
+ const stats = computeEnhancedStats(trades);
90
+ expect(stats.byExchangePair).toHaveLength(3);
91
+ // Sorted by trade count descending
92
+ const hlPac = stats.byExchangePair.find(p => p.pair === "HL/PAC");
93
+ expect(hlPac).toBeDefined();
94
+ expect(hlPac.trades).toBe(3);
95
+ expect(hlPac.winRate).toBeCloseTo(66.67, 0);
96
+ expect(hlPac.avgNetPnl).toBeCloseTo(4.33, 1);
97
+ const ltPac = stats.byExchangePair.find(p => p.pair === "LT/PAC");
98
+ expect(ltPac).toBeDefined();
99
+ expect(ltPac.trades).toBe(2);
100
+ expect(ltPac.winRate).toBe(100);
101
+ expect(ltPac.avgNetPnl).toBe(5.5);
102
+ const hlLt = stats.byExchangePair.find(p => p.pair === "HL/LT");
103
+ expect(hlLt).toBeDefined();
104
+ expect(hlLt.trades).toBe(1);
105
+ });
106
+ it("buckets by time of day correctly", () => {
107
+ const trades = [
108
+ makeTrade({ entryDate: "2025-01-15T01:00:00.000Z", netReturn: 12 }),
109
+ makeTrade({ entryDate: "2025-01-16T02:30:00.000Z", netReturn: 8 }),
110
+ makeTrade({ entryDate: "2025-01-17T03:00:00.000Z", netReturn: 16 }),
111
+ makeTrade({ entryDate: "2025-01-18T08:15:00.000Z", netReturn: 5 }),
112
+ makeTrade({ entryDate: "2025-01-19T09:00:00.000Z", netReturn: -3 }),
113
+ makeTrade({ entryDate: "2025-01-20T16:00:00.000Z", netReturn: -2 }),
114
+ makeTrade({ entryDate: "2025-01-20T18:30:00.000Z", netReturn: 1 }),
115
+ ];
116
+ const stats = computeEnhancedStats(trades);
117
+ const bucket00 = stats.byTimeOfDay.find(b => b.bucket === "00-04 UTC");
118
+ expect(bucket00).toBeDefined();
119
+ expect(bucket00.trades).toBe(3);
120
+ expect(bucket00.winRate).toBe(100);
121
+ expect(bucket00.avgNetPnl).toBe(12);
122
+ const bucket08 = stats.byTimeOfDay.find(b => b.bucket === "08-12 UTC");
123
+ expect(bucket08).toBeDefined();
124
+ expect(bucket08.trades).toBe(2);
125
+ expect(bucket08.winRate).toBe(50);
126
+ const bucket16 = stats.byTimeOfDay.find(b => b.bucket === "16-20 UTC");
127
+ expect(bucket16).toBeDefined();
128
+ expect(bucket16.trades).toBe(2);
129
+ expect(bucket16.avgNetPnl).toBe(-0.5);
130
+ });
131
+ it("computes optimal hold time as median of winning trades", () => {
132
+ const dayMs = 24 * 60 * 60 * 1000;
133
+ const trades = [
134
+ makeTrade({ holdDurationMs: 2 * dayMs, netReturn: 5 }), // win
135
+ makeTrade({ holdDurationMs: 4 * dayMs, netReturn: 10 }), // win
136
+ makeTrade({ holdDurationMs: 6 * dayMs, netReturn: 8 }), // win
137
+ makeTrade({ holdDurationMs: 8 * dayMs, netReturn: -3 }), // loss
138
+ makeTrade({ holdDurationMs: 10 * dayMs, netReturn: 12 }), // win
139
+ ];
140
+ const stats = computeEnhancedStats(trades);
141
+ // Profitable hold times sorted: 2d, 4d, 6d, 10d
142
+ // Median of 4 items: (4d + 6d) / 2 = 5d
143
+ expect(stats.optimalHoldTimeMs).toBe(5 * dayMs);
144
+ expect(stats.optimalHoldTime).toBe("5d 0h");
145
+ });
146
+ it("optimal hold time with odd number of winning trades", () => {
147
+ const dayMs = 24 * 60 * 60 * 1000;
148
+ const trades = [
149
+ makeTrade({ holdDurationMs: 3 * dayMs, netReturn: 2 }),
150
+ makeTrade({ holdDurationMs: 5 * dayMs, netReturn: 7 }),
151
+ makeTrade({ holdDurationMs: 7 * dayMs, netReturn: 1 }),
152
+ ];
153
+ const stats = computeEnhancedStats(trades);
154
+ // Median of [3d, 5d, 7d] = 5d
155
+ expect(stats.optimalHoldTimeMs).toBe(5 * dayMs);
156
+ expect(stats.optimalHoldTime).toBe("5d 0h");
157
+ });
158
+ it("optimal hold time is null when no profitable trades", () => {
159
+ const trades = [
160
+ makeTrade({ netReturn: -5 }),
161
+ makeTrade({ netReturn: -2 }),
162
+ ];
163
+ const stats = computeEnhancedStats(trades);
164
+ expect(stats.optimalHoldTime).toBeNull();
165
+ expect(stats.optimalHoldTimeMs).toBeNull();
166
+ });
167
+ it("skips empty time-of-day buckets", () => {
168
+ const trades = [
169
+ makeTrade({ entryDate: "2025-01-15T02:00:00.000Z" }),
170
+ ];
171
+ const stats = computeEnhancedStats(trades);
172
+ // Only the 00-04 bucket should appear
173
+ expect(stats.byTimeOfDay).toHaveLength(1);
174
+ expect(stats.byTimeOfDay[0].bucket).toBe("00-04 UTC");
175
+ });
176
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,84 @@
1
+ import { describe, it, expect } from "vitest";
2
+ function determinArbDirection(snap) {
3
+ // Short the high-funding exchange (get paid), long the low-funding one
4
+ const shortExchange = snap.pacRate > snap.hlRate ? "pacifica" : "hyperliquid";
5
+ const longExchange = snap.pacRate > snap.hlRate ? "hyperliquid" : "pacifica";
6
+ return { longExchange, shortExchange };
7
+ }
8
+ function computeAnnualizedSpread(pacRate, hlRate) {
9
+ return (pacRate - hlRate) * 24 * 365 * 100;
10
+ }
11
+ describe("Arb direction logic", () => {
12
+ it("shorts high-funding exchange (Pacifica higher)", () => {
13
+ const snap = { symbol: "BTC", pacRate: 0.001, hlRate: 0.0002, spread: 87.6 };
14
+ const { longExchange, shortExchange } = determinArbDirection(snap);
15
+ // Pacifica rate is higher → short Pacifica (get paid funding), long HL
16
+ expect(shortExchange).toBe("pacifica");
17
+ expect(longExchange).toBe("hyperliquid");
18
+ });
19
+ it("shorts high-funding exchange (Hyperliquid higher)", () => {
20
+ const snap = { symbol: "ETH", pacRate: 0.0001, hlRate: 0.0008, spread: -76.65 };
21
+ const { longExchange, shortExchange } = determinArbDirection(snap);
22
+ // HL rate is higher → short HL, long Pacifica
23
+ expect(shortExchange).toBe("hyperliquid");
24
+ expect(longExchange).toBe("pacifica");
25
+ });
26
+ it("handles equal rates", () => {
27
+ const snap = { symbol: "SOL", pacRate: 0.0003, hlRate: 0.0003, spread: 0 };
28
+ const { longExchange, shortExchange } = determinArbDirection(snap);
29
+ // Equal rates → default to long pacifica (pacRate > hlRate is false)
30
+ expect(longExchange).toBe("pacifica");
31
+ expect(shortExchange).toBe("hyperliquid");
32
+ });
33
+ it("handles negative funding rates", () => {
34
+ const snap = { symbol: "DOGE", pacRate: -0.001, hlRate: -0.0002, spread: -87.6 };
35
+ const { longExchange, shortExchange } = determinArbDirection(snap);
36
+ // Pacifica rate is MORE negative → hlRate > pacRate → short HL, long Pacifica
37
+ expect(shortExchange).toBe("hyperliquid");
38
+ expect(longExchange).toBe("pacifica");
39
+ });
40
+ it("handles mixed sign funding rates", () => {
41
+ const snap = { symbol: "ARB", pacRate: 0.001, hlRate: -0.0005, spread: 164.25 };
42
+ const { longExchange, shortExchange } = determinArbDirection(snap);
43
+ // Pac is positive (longs pay), HL is negative (shorts pay)
44
+ // Short Pac (get paid) + Long HL (get paid) = double collect!
45
+ expect(shortExchange).toBe("pacifica");
46
+ expect(longExchange).toBe("hyperliquid");
47
+ });
48
+ });
49
+ describe("Spread calculation", () => {
50
+ it("computes annualized spread correctly", () => {
51
+ // 0.01% per hour difference → 0.24% per day → 87.6% per year
52
+ const spread = computeAnnualizedSpread(0.0002, 0.0001);
53
+ expect(spread).toBeCloseTo(87.6, 1);
54
+ });
55
+ it("negative spread when HL rate is higher", () => {
56
+ const spread = computeAnnualizedSpread(0.0001, 0.0003);
57
+ expect(spread).toBeLessThan(0);
58
+ expect(Math.abs(spread)).toBeCloseTo(175.2, 1);
59
+ });
60
+ it("zero spread when rates are equal", () => {
61
+ const spread = computeAnnualizedSpread(0.0001, 0.0001);
62
+ expect(spread).toBe(0);
63
+ });
64
+ });
65
+ describe("Entry/exit conditions", () => {
66
+ const minSpread = 30; // 30% annual
67
+ const closeSpread = 5; // 5% annual
68
+ it("should enter when spread exceeds threshold", () => {
69
+ const absSpread = 45; // 45% > 30%
70
+ expect(absSpread >= minSpread).toBe(true);
71
+ });
72
+ it("should NOT enter when spread below threshold", () => {
73
+ const absSpread = 20; // 20% < 30%
74
+ expect(absSpread >= minSpread).toBe(false);
75
+ });
76
+ it("should close when spread drops below close threshold", () => {
77
+ const currentSpread = 3; // 3% < 5%
78
+ expect(currentSpread <= closeSpread).toBe(true);
79
+ });
80
+ it("should NOT close when spread is still profitable", () => {
81
+ const currentSpread = 15; // 15% > 5%
82
+ expect(currentSpread <= closeSpread).toBe(false);
83
+ });
84
+ });
@@ -0,0 +1 @@
1
+ export {};