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,137 @@
1
+ /**
2
+ * Mock adapter for unit testing. Records all calls and returns configurable responses.
3
+ */
4
+ export class MockAdapter {
5
+ name;
6
+ calls = [];
7
+ // Configurable return values
8
+ marketsResponse = [];
9
+ orderbookResponse = { bids: [], asks: [] };
10
+ balanceResponse = { equity: "1000", available: "800", marginUsed: "200", unrealizedPnl: "50" };
11
+ positionsResponse = [];
12
+ ordersResponse = [];
13
+ orderResult = { status: "ok", orderId: "12345" };
14
+ constructor(name = "mock") {
15
+ this.name = name;
16
+ }
17
+ record(method, args) {
18
+ this.calls.push({ method, args });
19
+ }
20
+ getCallsFor(method) {
21
+ return this.calls.filter((c) => c.method === method);
22
+ }
23
+ reset() {
24
+ this.calls = [];
25
+ }
26
+ async getMarkets() {
27
+ this.record("getMarkets", []);
28
+ return this.marketsResponse;
29
+ }
30
+ async getOrderbook(symbol) {
31
+ this.record("getOrderbook", [symbol]);
32
+ return this.orderbookResponse;
33
+ }
34
+ async getBalance() {
35
+ this.record("getBalance", []);
36
+ return this.balanceResponse;
37
+ }
38
+ async getPositions() {
39
+ this.record("getPositions", []);
40
+ return this.positionsResponse;
41
+ }
42
+ async getOpenOrders() {
43
+ this.record("getOpenOrders", []);
44
+ return this.ordersResponse;
45
+ }
46
+ async marketOrder(symbol, side, size) {
47
+ this.record("marketOrder", [symbol, side, size]);
48
+ return this.orderResult;
49
+ }
50
+ async limitOrder(symbol, side, price, size) {
51
+ this.record("limitOrder", [symbol, side, price, size]);
52
+ return this.orderResult;
53
+ }
54
+ async editOrder(symbol, orderId, price, size) {
55
+ this.record("editOrder", [symbol, orderId, price, size]);
56
+ return this.orderResult;
57
+ }
58
+ async cancelOrder(symbol, orderId) {
59
+ this.record("cancelOrder", [symbol, orderId]);
60
+ return { status: "cancelled" };
61
+ }
62
+ async cancelAllOrders(symbol) {
63
+ this.record("cancelAllOrders", [symbol]);
64
+ return { status: "all_cancelled" };
65
+ }
66
+ async setLeverage(symbol, leverage, marginMode) {
67
+ this.record("setLeverage", [symbol, leverage, marginMode]);
68
+ return { symbol, leverage, marginMode };
69
+ }
70
+ async stopOrder(symbol, side, size, triggerPrice, opts) {
71
+ this.record("stopOrder", [symbol, side, size, triggerPrice, opts]);
72
+ return this.orderResult;
73
+ }
74
+ async getRecentTrades(symbol, limit) {
75
+ this.record("getRecentTrades", [symbol, limit]);
76
+ return [];
77
+ }
78
+ async getFundingHistory(symbol, limit) {
79
+ this.record("getFundingHistory", [symbol, limit]);
80
+ return [];
81
+ }
82
+ async getKlines(symbol, interval, startTime, endTime) {
83
+ this.record("getKlines", [symbol, interval, startTime, endTime]);
84
+ return [];
85
+ }
86
+ async getOrderHistory(limit) {
87
+ this.record("getOrderHistory", [limit]);
88
+ return this.ordersResponse;
89
+ }
90
+ async getTradeHistory(limit) {
91
+ this.record("getTradeHistory", [limit]);
92
+ return [];
93
+ }
94
+ async getFundingPayments(limit) {
95
+ this.record("getFundingPayments", [limit]);
96
+ return [];
97
+ }
98
+ }
99
+ /** Factory to create mock markets data */
100
+ export function createMockMarkets(count = 3) {
101
+ const symbols = ["BTC", "ETH", "SOL", "DOGE", "ARB"];
102
+ return symbols.slice(0, count).map((symbol) => ({
103
+ symbol,
104
+ markPrice: String(symbol === "BTC" ? 100000 : symbol === "ETH" ? 3500 : 150),
105
+ indexPrice: String(symbol === "BTC" ? 99990 : symbol === "ETH" ? 3498 : 149.5),
106
+ fundingRate: "0.0001",
107
+ volume24h: "1000000",
108
+ openInterest: "500000",
109
+ maxLeverage: symbol === "BTC" ? 100 : 50,
110
+ }));
111
+ }
112
+ /** Factory to create mock positions */
113
+ export function createMockPositions(count = 1) {
114
+ return Array.from({ length: count }, (_, i) => ({
115
+ symbol: i === 0 ? "BTC" : "ETH",
116
+ side: (i % 2 === 0 ? "long" : "short"),
117
+ size: "0.1",
118
+ entryPrice: "100000",
119
+ markPrice: "101000",
120
+ liquidationPrice: "90000",
121
+ unrealizedPnl: "100",
122
+ leverage: 10,
123
+ }));
124
+ }
125
+ /** Factory to create mock orders */
126
+ export function createMockOrders(count = 2) {
127
+ return Array.from({ length: count }, (_, i) => ({
128
+ orderId: String(1000 + i),
129
+ symbol: "BTC",
130
+ side: (i % 2 === 0 ? "buy" : "sell"),
131
+ price: String(99000 + i * 1000),
132
+ size: "0.05",
133
+ filled: "0",
134
+ status: "open",
135
+ type: "limit",
136
+ }));
137
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,106 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { logExecution, readExecutionLog, getExecutionStats } from "../execution-log.js";
3
+ import { existsSync, unlinkSync, readFileSync, writeFileSync, renameSync } from "fs";
4
+ import { resolve } from "path";
5
+ const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
6
+ const LOG_FILE = resolve(PERP_DIR, "executions.jsonl");
7
+ const BACKUP_FILE = resolve(PERP_DIR, "executions.jsonl.test-backup");
8
+ describe("Execution Log", () => {
9
+ beforeEach(() => {
10
+ // Backup existing log if present
11
+ if (existsSync(LOG_FILE)) {
12
+ const content = readFileSync(LOG_FILE, "utf-8");
13
+ writeFileSync(BACKUP_FILE, content);
14
+ }
15
+ // Clear log for test
16
+ if (existsSync(LOG_FILE))
17
+ unlinkSync(LOG_FILE);
18
+ });
19
+ afterEach(() => {
20
+ // Restore original log
21
+ if (existsSync(LOG_FILE))
22
+ unlinkSync(LOG_FILE);
23
+ if (existsSync(BACKUP_FILE)) {
24
+ renameSync(BACKUP_FILE, LOG_FILE);
25
+ }
26
+ });
27
+ it("should log an execution record", () => {
28
+ const record = logExecution({
29
+ type: "market_order",
30
+ exchange: "hyperliquid",
31
+ symbol: "BTC",
32
+ side: "buy",
33
+ size: "0.1",
34
+ price: "100000",
35
+ notional: 10000,
36
+ status: "success",
37
+ dryRun: false,
38
+ });
39
+ expect(record.id).toBeTruthy();
40
+ expect(record.timestamp).toBeTruthy();
41
+ expect(record.exchange).toBe("hyperliquid");
42
+ expect(record.symbol).toBe("BTC");
43
+ });
44
+ it("should read back logged records", () => {
45
+ logExecution({ type: "market_order", exchange: "test", symbol: "BTC", side: "buy", size: "0.1", status: "success", dryRun: false });
46
+ logExecution({ type: "limit_order", exchange: "test", symbol: "ETH", side: "sell", size: "1.0", status: "success", dryRun: false });
47
+ const records = readExecutionLog();
48
+ expect(records).toHaveLength(2);
49
+ });
50
+ it("should filter by exchange", () => {
51
+ logExecution({ type: "market_order", exchange: "hl", symbol: "BTC", side: "buy", size: "0.1", status: "success", dryRun: false });
52
+ logExecution({ type: "market_order", exchange: "pac", symbol: "BTC", side: "buy", size: "0.1", status: "success", dryRun: false });
53
+ const records = readExecutionLog({ exchange: "hl" });
54
+ expect(records).toHaveLength(1);
55
+ expect(records[0].exchange).toBe("hl");
56
+ });
57
+ it("should filter by symbol", () => {
58
+ logExecution({ type: "market_order", exchange: "test", symbol: "BTC", side: "buy", size: "0.1", status: "success", dryRun: false });
59
+ logExecution({ type: "market_order", exchange: "test", symbol: "ETH", side: "sell", size: "1.0", status: "success", dryRun: false });
60
+ const records = readExecutionLog({ symbol: "ETH" });
61
+ expect(records).toHaveLength(1);
62
+ expect(records[0].symbol).toBe("ETH");
63
+ });
64
+ it("should mark dry-run executions", () => {
65
+ logExecution({ type: "market_order", exchange: "test", symbol: "BTC", side: "buy", size: "0.1", status: "simulated", dryRun: true });
66
+ logExecution({ type: "market_order", exchange: "test", symbol: "ETH", side: "sell", size: "1.0", status: "success", dryRun: false });
67
+ const dryRuns = readExecutionLog({ dryRunOnly: true });
68
+ expect(dryRuns).toHaveLength(1);
69
+ expect(dryRuns[0].dryRun).toBe(true);
70
+ expect(dryRuns[0].status).toBe("simulated");
71
+ });
72
+ it("should compute execution stats", () => {
73
+ logExecution({ type: "market_order", exchange: "hl", symbol: "BTC", side: "buy", size: "0.1", status: "success", dryRun: false });
74
+ logExecution({ type: "limit_order", exchange: "pac", symbol: "ETH", side: "sell", size: "1.0", status: "success", dryRun: false });
75
+ logExecution({ type: "market_order", exchange: "hl", symbol: "SOL", side: "buy", size: "10", status: "failed", error: "insufficient balance", dryRun: false });
76
+ const stats = getExecutionStats();
77
+ expect(stats.totalTrades).toBe(3);
78
+ expect(stats.successRate).toBeCloseTo(66.67, 0);
79
+ expect(stats.byExchange.hl).toBe(2);
80
+ expect(stats.byExchange.pac).toBe(1);
81
+ expect(stats.byType.market_order).toBe(2);
82
+ expect(stats.byType.limit_order).toBe(1);
83
+ expect(stats.recentErrors).toHaveLength(1);
84
+ });
85
+ it("should return empty stats when no log file", () => {
86
+ const stats = getExecutionStats();
87
+ expect(stats.totalTrades).toBe(0);
88
+ expect(stats.successRate).toBe(0);
89
+ });
90
+ it("should limit results", () => {
91
+ for (let i = 0; i < 10; i++) {
92
+ logExecution({ type: "market_order", exchange: "test", symbol: "BTC", side: "buy", size: "0.1", status: "success", dryRun: false });
93
+ }
94
+ const records = readExecutionLog({ limit: 3 });
95
+ expect(records).toHaveLength(3);
96
+ });
97
+ it("should sort newest first", async () => {
98
+ logExecution({ type: "market_order", exchange: "test", symbol: "FIRST", side: "buy", size: "0.1", status: "success", dryRun: false });
99
+ // Ensure distinct timestamps (Date.now resolution is 1ms)
100
+ await new Promise((r) => setTimeout(r, 5));
101
+ logExecution({ type: "market_order", exchange: "test", symbol: "SECOND", side: "buy", size: "0.1", status: "success", dryRun: false });
102
+ const records = readExecutionLog();
103
+ expect(records[0].symbol).toBe("SECOND");
104
+ expect(records[1].symbol).toBe("FIRST");
105
+ });
106
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { annualizeRate, computeAnnualSpread, toHourlyRate, estimateHourlyFunding } from "../funding.js";
3
+ describe("Funding Rate Normalization", () => {
4
+ describe("toHourlyRate", () => {
5
+ it("should return HL rate as-is (already hourly)", () => {
6
+ expect(toHourlyRate(0.001, "hyperliquid")).toBeCloseTo(0.001);
7
+ });
8
+ it("should return Pacifica rate as-is (already hourly)", () => {
9
+ expect(toHourlyRate(0.001, "pacifica")).toBeCloseTo(0.001);
10
+ });
11
+ it("should return Lighter rate as-is (already hourly)", () => {
12
+ expect(toHourlyRate(0.001, "lighter")).toBeCloseTo(0.001);
13
+ });
14
+ });
15
+ describe("annualizeRate", () => {
16
+ it("should annualize HL hourly rate correctly", () => {
17
+ // 0.01% per hour * 8760 hours = 87.6%
18
+ const result = annualizeRate(0.0001, "hyperliquid");
19
+ expect(result).toBeCloseTo(87.6, 0);
20
+ });
21
+ it("should annualize Pacifica hourly rate correctly", () => {
22
+ // 0.01% per hour * 8760 = 87.6%
23
+ const result = annualizeRate(0.0001, "pacifica");
24
+ expect(result).toBeCloseTo(87.6, 0);
25
+ });
26
+ it("should handle zero rate", () => {
27
+ expect(annualizeRate(0, "hyperliquid")).toBe(0);
28
+ });
29
+ it("should handle negative rates", () => {
30
+ expect(annualizeRate(-0.0001, "hyperliquid")).toBeLessThan(0);
31
+ });
32
+ });
33
+ describe("computeAnnualSpread", () => {
34
+ it("should compute spread between different exchanges", () => {
35
+ // HL: 0.01% hourly, PAC: 0.001% hourly
36
+ // Spread = |0.0001 - 0.00001| * 8760 * 100 = ~78.84%
37
+ const spread = computeAnnualSpread(0.0001, "hyperliquid", 0.00001, "pacifica");
38
+ expect(spread).toBeGreaterThan(0);
39
+ });
40
+ it("should return positive spread when rates are ordered correctly", () => {
41
+ const spread = computeAnnualSpread(0.001, "hyperliquid", 0.0001, "hyperliquid");
42
+ expect(spread).toBeGreaterThan(0);
43
+ });
44
+ it("should handle same exchange same rate (zero spread)", () => {
45
+ const spread = computeAnnualSpread(0.0001, "hyperliquid", 0.0001, "hyperliquid");
46
+ expect(spread).toBeCloseTo(0, 1);
47
+ });
48
+ it("should compute zero spread for identical hourly rates", () => {
49
+ // Same hourly rate on both exchanges
50
+ const spread = computeAnnualSpread(0.001, "hyperliquid", 0.001, "pacifica");
51
+ expect(Math.abs(spread)).toBeLessThan(1); // should be ~0
52
+ });
53
+ });
54
+ describe("estimateHourlyFunding", () => {
55
+ it("should estimate funding cost for long position (positive rate = longs pay)", () => {
56
+ // Positive rate, long position = pay funding (positive return means you pay)
57
+ const result = estimateHourlyFunding(0.0001, "hyperliquid", 10000, "long");
58
+ expect(result).toBeGreaterThan(0); // longs pay when rate is positive
59
+ });
60
+ it("should estimate funding income for short position (positive rate = shorts receive)", () => {
61
+ // Positive rate, short position = receive funding (negative return means you receive)
62
+ const result = estimateHourlyFunding(0.0001, "hyperliquid", 10000, "short");
63
+ expect(result).toBeLessThan(0); // shorts receive when rate is positive
64
+ });
65
+ it("should scale with position size", () => {
66
+ const small = estimateHourlyFunding(0.0001, "hyperliquid", 1000, "long");
67
+ const big = estimateHourlyFunding(0.0001, "hyperliquid", 10000, "long");
68
+ expect(Math.abs(big)).toBeCloseTo(Math.abs(small) * 10, 2);
69
+ });
70
+ });
71
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,343 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ // Use vi.hoisted so TEST_DIR is available when vi.mock runs (hoisted)
5
+ const { TEST_DIR } = vi.hoisted(() => {
6
+ const { tmpdir } = require("node:os");
7
+ const { join } = require("node:path");
8
+ return {
9
+ TEST_DIR: join(tmpdir(), `perp-funding-test-${Date.now()}-${Math.random().toString(36).slice(2)}`),
10
+ };
11
+ });
12
+ // Mock the homedir to redirect storage to our test directory
13
+ vi.mock("node:os", async () => {
14
+ const actual = await vi.importActual("node:os");
15
+ return {
16
+ ...actual,
17
+ homedir: () => join(TEST_DIR, "home"),
18
+ };
19
+ });
20
+ import { saveFundingSnapshot, getAvgFundingRate, getHistoricalRates, getHistoricalAverages, getCompoundedAnnualReturn, getExchangeCompoundingHours, cleanupOldFiles, _resetCleanupFlag, } from "../funding-history.js";
21
+ const DATA_DIR = join(TEST_DIR, "home", ".perp", "funding-rates");
22
+ function makeRate(overrides = {}) {
23
+ return {
24
+ exchange: "hyperliquid",
25
+ symbol: "BTC",
26
+ fundingRate: 0.0001,
27
+ hourlyRate: 0.0001,
28
+ annualizedPct: 87.6,
29
+ markPrice: 50000,
30
+ ...overrides,
31
+ };
32
+ }
33
+ function readJsonl(filePath) {
34
+ if (!existsSync(filePath))
35
+ return [];
36
+ const content = readFileSync(filePath, "utf-8");
37
+ return content
38
+ .split("\n")
39
+ .filter(l => l.trim())
40
+ .map(l => JSON.parse(l));
41
+ }
42
+ function getMonthKey(date) {
43
+ const y = date.getFullYear();
44
+ const m = String(date.getMonth() + 1).padStart(2, "0");
45
+ return `${y}-${m}`;
46
+ }
47
+ describe("funding-history", () => {
48
+ beforeEach(() => {
49
+ _resetCleanupFlag();
50
+ mkdirSync(DATA_DIR, { recursive: true });
51
+ });
52
+ afterEach(() => {
53
+ try {
54
+ rmSync(TEST_DIR, { recursive: true, force: true });
55
+ }
56
+ catch {
57
+ // ignore cleanup errors
58
+ }
59
+ });
60
+ // ──────────────────────────────────────────────
61
+ // saveFundingSnapshot
62
+ // ──────────────────────────────────────────────
63
+ describe("saveFundingSnapshot", () => {
64
+ it("writes JSONL correctly", () => {
65
+ const rates = [
66
+ makeRate({ exchange: "hyperliquid", symbol: "BTC", fundingRate: 0.0001, hourlyRate: 0.0001 }),
67
+ makeRate({ exchange: "pacifica", symbol: "ETH", fundingRate: 0.0008, hourlyRate: 0.0001 }),
68
+ ];
69
+ saveFundingSnapshot(rates);
70
+ const monthKey = getMonthKey(new Date());
71
+ const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
72
+ expect(existsSync(filePath)).toBe(true);
73
+ const entries = readJsonl(filePath);
74
+ expect(entries).toHaveLength(2);
75
+ expect(entries[0].symbol).toBe("BTC");
76
+ expect(entries[0].exchange).toBe("hyperliquid");
77
+ expect(entries[0].rate).toBe(0.0001);
78
+ expect(entries[0].hourlyRate).toBe(0.0001);
79
+ expect(entries[0].ts).toBeDefined();
80
+ expect(entries[1].symbol).toBe("ETH");
81
+ expect(entries[1].exchange).toBe("pacifica");
82
+ });
83
+ it("deduplicates entries within 5 minutes", () => {
84
+ const rates = [makeRate({ symbol: "BTC", exchange: "hyperliquid" })];
85
+ // Save twice in quick succession
86
+ saveFundingSnapshot(rates);
87
+ saveFundingSnapshot(rates);
88
+ const monthKey = getMonthKey(new Date());
89
+ const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
90
+ const entries = readJsonl(filePath);
91
+ // Should only have 1 entry due to dedup
92
+ expect(entries).toHaveLength(1);
93
+ });
94
+ it("allows entries after 5 minute gap", () => {
95
+ const rates = [makeRate({ symbol: "BTC", exchange: "hyperliquid" })];
96
+ // Write an entry with a timestamp 6 minutes ago
97
+ const monthKey = getMonthKey(new Date());
98
+ const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
99
+ const oldTs = new Date(Date.now() - 6 * 60 * 1000).toISOString();
100
+ const oldEntry = {
101
+ ts: oldTs,
102
+ symbol: "BTC",
103
+ exchange: "hyperliquid",
104
+ rate: 0.0001,
105
+ hourlyRate: 0.0001,
106
+ };
107
+ writeFileSync(filePath, JSON.stringify(oldEntry) + "\n");
108
+ // Now save new snapshot
109
+ saveFundingSnapshot(rates);
110
+ const entries = readJsonl(filePath);
111
+ expect(entries).toHaveLength(2);
112
+ });
113
+ it("uppercases symbol names", () => {
114
+ const rates = [makeRate({ symbol: "btc" })];
115
+ saveFundingSnapshot(rates);
116
+ const monthKey = getMonthKey(new Date());
117
+ const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
118
+ const entries = readJsonl(filePath);
119
+ expect(entries[0].symbol).toBe("BTC");
120
+ });
121
+ it("skips entries with empty symbol", () => {
122
+ const rates = [makeRate({ symbol: "" })];
123
+ saveFundingSnapshot(rates);
124
+ const monthKey = getMonthKey(new Date());
125
+ const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
126
+ // File may or may not exist; if it does, it should be empty
127
+ if (existsSync(filePath)) {
128
+ const entries = readJsonl(filePath);
129
+ expect(entries).toHaveLength(0);
130
+ }
131
+ });
132
+ });
133
+ // ──────────────────────────────────────────────
134
+ // getAvgFundingRate
135
+ // ──────────────────────────────────────────────
136
+ describe("getAvgFundingRate", () => {
137
+ it("returns correct average", () => {
138
+ const monthKey = getMonthKey(new Date());
139
+ const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
140
+ // Write 3 entries with different hourly rates at recent times
141
+ const entries = [
142
+ { ts: new Date(Date.now() - 30 * 60 * 1000).toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0001, hourlyRate: 0.0001 },
143
+ { ts: new Date(Date.now() - 20 * 60 * 1000).toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0002, hourlyRate: 0.0002 },
144
+ { ts: new Date(Date.now() - 10 * 60 * 1000).toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0003, hourlyRate: 0.0003 },
145
+ ];
146
+ writeFileSync(filePath, entries.map(e => JSON.stringify(e)).join("\n") + "\n");
147
+ const avg = getAvgFundingRate("BTC", "hyperliquid", 1);
148
+ expect(avg).toBeCloseTo(0.0002); // (0.0001 + 0.0002 + 0.0003) / 3
149
+ });
150
+ it("returns null when no data available", () => {
151
+ const avg = getAvgFundingRate("NOEXIST", "hyperliquid", 24);
152
+ expect(avg).toBeNull();
153
+ });
154
+ it("filters by symbol and exchange correctly", () => {
155
+ const monthKey = getMonthKey(new Date());
156
+ const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
157
+ const entries = [
158
+ { ts: new Date(Date.now() - 10 * 60 * 1000).toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0001, hourlyRate: 0.0001 },
159
+ { ts: new Date(Date.now() - 10 * 60 * 1000).toISOString(), symbol: "ETH", exchange: "hyperliquid", rate: 0.0005, hourlyRate: 0.0005 },
160
+ { ts: new Date(Date.now() - 10 * 60 * 1000).toISOString(), symbol: "BTC", exchange: "pacifica", rate: 0.0008, hourlyRate: 0.0001 },
161
+ ];
162
+ writeFileSync(filePath, entries.map(e => JSON.stringify(e)).join("\n") + "\n");
163
+ const avgBtcHL = getAvgFundingRate("BTC", "hyperliquid", 1);
164
+ expect(avgBtcHL).toBeCloseTo(0.0001);
165
+ const avgEthHL = getAvgFundingRate("ETH", "hyperliquid", 1);
166
+ expect(avgEthHL).toBeCloseTo(0.0005);
167
+ const avgBtcPac = getAvgFundingRate("BTC", "pacifica", 1);
168
+ expect(avgBtcPac).toBeCloseTo(0.0001);
169
+ });
170
+ it("only includes entries within the time window", () => {
171
+ const monthKey = getMonthKey(new Date());
172
+ const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
173
+ const entries = [
174
+ { ts: new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.001, hourlyRate: 0.001 },
175
+ { ts: new Date(Date.now() - 10 * 60 * 1000).toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0001, hourlyRate: 0.0001 },
176
+ ];
177
+ writeFileSync(filePath, entries.map(e => JSON.stringify(e)).join("\n") + "\n");
178
+ // Only 1h window should exclude the 3h-old entry
179
+ const avg1h = getAvgFundingRate("BTC", "hyperliquid", 1);
180
+ expect(avg1h).toBeCloseTo(0.0001);
181
+ // 4h window should include both
182
+ const avg4h = getAvgFundingRate("BTC", "hyperliquid", 4);
183
+ expect(avg4h).toBeCloseTo(0.00055); // (0.001 + 0.0001) / 2
184
+ });
185
+ });
186
+ // ──────────────────────────────────────────────
187
+ // getHistoricalRates
188
+ // ──────────────────────────────────────────────
189
+ describe("getHistoricalRates", () => {
190
+ it("returns sorted entries in time range", () => {
191
+ const monthKey = getMonthKey(new Date());
192
+ const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
193
+ const t1 = new Date(Date.now() - 30 * 60 * 1000);
194
+ const t2 = new Date(Date.now() - 20 * 60 * 1000);
195
+ const t3 = new Date(Date.now() - 10 * 60 * 1000);
196
+ const entries = [
197
+ { ts: t3.toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0003, hourlyRate: 0.0003 },
198
+ { ts: t1.toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0001, hourlyRate: 0.0001 },
199
+ { ts: t2.toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0002, hourlyRate: 0.0002 },
200
+ ];
201
+ writeFileSync(filePath, entries.map(e => JSON.stringify(e)).join("\n") + "\n");
202
+ const startTime = new Date(Date.now() - 60 * 60 * 1000);
203
+ const endTime = new Date();
204
+ const result = getHistoricalRates("BTC", "hyperliquid", startTime, endTime);
205
+ expect(result).toHaveLength(3);
206
+ // Should be sorted by time ascending
207
+ expect(result[0].rate).toBe(0.0001);
208
+ expect(result[1].rate).toBe(0.0002);
209
+ expect(result[2].rate).toBe(0.0003);
210
+ });
211
+ });
212
+ // ──────────────────────────────────────────────
213
+ // getHistoricalAverages
214
+ // ──────────────────────────────────────────────
215
+ describe("getHistoricalAverages", () => {
216
+ it("handles missing data (returns null)", () => {
217
+ const result = getHistoricalAverages(["BTC"], ["hyperliquid"]);
218
+ const avgs = result.get("BTC:hyperliquid");
219
+ expect(avgs).toBeDefined();
220
+ expect(avgs.avg1h).toBeNull();
221
+ expect(avgs.avg8h).toBeNull();
222
+ expect(avgs.avg24h).toBeNull();
223
+ expect(avgs.avg7d).toBeNull();
224
+ });
225
+ it("computes averages for different time windows", () => {
226
+ const monthKey = getMonthKey(new Date());
227
+ const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
228
+ const entries = [
229
+ // Within 1h
230
+ { ts: new Date(Date.now() - 30 * 60 * 1000).toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0002, hourlyRate: 0.0002 },
231
+ // Within 8h but not 1h
232
+ { ts: new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0004, hourlyRate: 0.0004 },
233
+ ];
234
+ writeFileSync(filePath, entries.map(e => JSON.stringify(e)).join("\n") + "\n");
235
+ const result = getHistoricalAverages(["BTC"], ["hyperliquid"]);
236
+ const avgs = result.get("BTC:hyperliquid");
237
+ expect(avgs).toBeDefined();
238
+ expect(avgs.avg1h).toBeCloseTo(0.0002); // only the 30-min-old entry
239
+ expect(avgs.avg8h).toBeCloseTo(0.0003); // both entries: (0.0002 + 0.0004) / 2
240
+ expect(avgs.avg24h).toBeCloseTo(0.0003); // both entries
241
+ expect(avgs.avg7d).toBeCloseTo(0.0003); // both entries
242
+ });
243
+ it("generates correct keys for multiple symbols and exchanges", () => {
244
+ const result = getHistoricalAverages(["BTC", "ETH"], ["hyperliquid", "pacifica"]);
245
+ expect(result.has("BTC:hyperliquid")).toBe(true);
246
+ expect(result.has("BTC:pacifica")).toBe(true);
247
+ expect(result.has("ETH:hyperliquid")).toBe(true);
248
+ expect(result.has("ETH:pacifica")).toBe(true);
249
+ });
250
+ });
251
+ // ──────────────────────────────────────────────
252
+ // cleanupOldFiles
253
+ // ──────────────────────────────────────────────
254
+ describe("cleanupOldFiles", () => {
255
+ it("removes files older than 30 days", () => {
256
+ // Create a file from 3 months ago
257
+ const oldDate = new Date();
258
+ oldDate.setMonth(oldDate.getMonth() - 3);
259
+ const oldKey = getMonthKey(oldDate);
260
+ const oldFilePath = join(DATA_DIR, `${oldKey}.jsonl`);
261
+ writeFileSync(oldFilePath, '{"ts":"old","symbol":"BTC","exchange":"hl","rate":0.01,"hourlyRate":0.01}\n');
262
+ // Create a current month file
263
+ const curKey = getMonthKey(new Date());
264
+ const curFilePath = join(DATA_DIR, `${curKey}.jsonl`);
265
+ writeFileSync(curFilePath, '{"ts":"now","symbol":"BTC","exchange":"hl","rate":0.01,"hourlyRate":0.01}\n');
266
+ _resetCleanupFlag();
267
+ cleanupOldFiles();
268
+ expect(existsSync(oldFilePath)).toBe(false);
269
+ expect(existsSync(curFilePath)).toBe(true);
270
+ });
271
+ it("keeps recent files", () => {
272
+ const curKey = getMonthKey(new Date());
273
+ const curFilePath = join(DATA_DIR, `${curKey}.jsonl`);
274
+ writeFileSync(curFilePath, '{"ts":"now","symbol":"BTC","exchange":"hl","rate":0.01,"hourlyRate":0.01}\n');
275
+ _resetCleanupFlag();
276
+ cleanupOldFiles();
277
+ expect(existsSync(curFilePath)).toBe(true);
278
+ });
279
+ });
280
+ // ──────────────────────────────────────────────
281
+ // getCompoundedAnnualReturn
282
+ // ──────────────────────────────────────────────
283
+ describe("getCompoundedAnnualReturn", () => {
284
+ it("calculates correct compounded return for HL (1h compounding)", () => {
285
+ // hourlyRate = 0.01% = 0.0001
286
+ // periodRate = 0.0001 * 1 = 0.0001
287
+ // periodsPerYear = 8760/1 = 8760
288
+ // (1 + 0.0001)^8760 - 1
289
+ const result = getCompoundedAnnualReturn(0.0001, 1);
290
+ // Expected: (1.0001)^8760 - 1 = ~1.3964 (139.64%)
291
+ expect(result).toBeCloseTo(Math.pow(1.0001, 8760) - 1, 2);
292
+ expect(result).toBeGreaterThan(0.876); // Should be > simple rate of 87.6%
293
+ });
294
+ it("calculates correct compounded return for PAC/LT (1h compounding, same as HL)", () => {
295
+ // hourlyRate = 0.0001
296
+ // periodRate = 0.0001 * 1 = 0.0001
297
+ // periodsPerYear = 8760/1 = 8760
298
+ // (1 + 0.0001)^8760 - 1
299
+ const result = getCompoundedAnnualReturn(0.0001, 1);
300
+ expect(result).toBeCloseTo(Math.pow(1.0001, 8760) - 1, 2);
301
+ });
302
+ it("returns 0 for zero rate", () => {
303
+ expect(getCompoundedAnnualReturn(0, 1)).toBe(0);
304
+ });
305
+ it("handles negative rates", () => {
306
+ const result = getCompoundedAnnualReturn(-0.0001, 1);
307
+ // (1 - 0.0001)^8760 - 1 should be negative
308
+ expect(result).toBeLessThan(0);
309
+ expect(result).toBeCloseTo(Math.pow(1 - 0.0001, 8760) - 1, 2);
310
+ });
311
+ it("all exchanges compound at the same frequency (1h)", () => {
312
+ const hourlyRate = 0.0001;
313
+ const hl = getCompoundedAnnualReturn(hourlyRate, 1); // compound every 1h
314
+ const pac = getCompoundedAnnualReturn(hourlyRate, 1); // compound every 1h (same as HL)
315
+ // Same compounding frequency, same effective annual return
316
+ expect(hl).toBeCloseTo(pac, 10);
317
+ });
318
+ it("simple rate sanity check: small rate, compounded vs simple", () => {
319
+ const hourlyRate = 0.0001;
320
+ const simpleAnnual = hourlyRate * 8760; // 0.876
321
+ const compoundedAnnual = getCompoundedAnnualReturn(hourlyRate, 1);
322
+ // Compounded should always be greater than simple for positive rates
323
+ expect(compoundedAnnual).toBeGreaterThan(simpleAnnual);
324
+ });
325
+ });
326
+ // ──────────────────────────────────────────────
327
+ // getExchangeCompoundingHours
328
+ // ──────────────────────────────────────────────
329
+ describe("getExchangeCompoundingHours", () => {
330
+ it("returns 1 for hyperliquid", () => {
331
+ expect(getExchangeCompoundingHours("hyperliquid")).toBe(1);
332
+ });
333
+ it("returns 1 for pacifica", () => {
334
+ expect(getExchangeCompoundingHours("pacifica")).toBe(1);
335
+ });
336
+ it("returns 1 for lighter", () => {
337
+ expect(getExchangeCompoundingHours("lighter")).toBe(1);
338
+ });
339
+ it("returns 1 for unknown exchanges", () => {
340
+ expect(getExchangeCompoundingHours("binance")).toBe(1);
341
+ });
342
+ });
343
+ });