perp-cli 0.11.0 → 0.12.0

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