perp-cli 0.9.8 → 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 (254) 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/bot/engine.js +23 -4
  21. package/dist/bot/presets.js +3 -3
  22. package/dist/bot/strategies/funding-arb-strategy.d.ts +0 -23
  23. package/dist/bot/strategies/funding-arb-strategy.js +41 -176
  24. package/dist/bot/strategies/funding-auto.js +2 -2
  25. package/dist/bridge-engine.js +102 -49
  26. package/dist/cli-spec.js +1 -2
  27. package/dist/commands/account.js +2 -500
  28. package/dist/commands/agent.d.ts +8 -2
  29. package/dist/commands/agent.js +1903 -366
  30. package/dist/commands/alerts.js +3 -1
  31. package/dist/commands/arb/index.js +37 -7
  32. package/dist/commands/arb-auto.js +44 -11
  33. package/dist/commands/arb.js +83 -34
  34. package/dist/commands/bot.d.ts +1 -1
  35. package/dist/commands/bot.js +20 -16
  36. package/dist/commands/bridge.d.ts +1 -1
  37. package/dist/commands/bridge.js +4 -4
  38. package/dist/commands/funds.d.ts +1 -1
  39. package/dist/commands/funds.js +18 -68
  40. package/dist/commands/jobs.d.ts +1 -1
  41. package/dist/commands/jobs.js +2 -2
  42. package/dist/commands/manage.d.ts +1 -1
  43. package/dist/commands/manage.js +12 -84
  44. package/dist/commands/plan.d.ts +1 -1
  45. package/dist/commands/plan.js +2 -2
  46. package/dist/commands/portfolio.d.ts +19 -3
  47. package/dist/commands/portfolio.js +747 -163
  48. package/dist/commands/rebalance.d.ts +1 -1
  49. package/dist/commands/rebalance.js +18 -12
  50. package/dist/commands/risk.js +2 -6
  51. package/dist/commands/settings.js +30 -53
  52. package/dist/commands/trade.js +16 -15
  53. package/dist/commands/wallet.d.ts +2 -1
  54. package/dist/commands/wallet.js +1122 -176
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +55 -9
  57. package/dist/errors.d.ts +64 -0
  58. package/dist/errors.js +82 -2
  59. package/dist/event-stream.js +12 -1
  60. package/dist/exchanges/aster-typed-data.d.ts +156 -0
  61. package/dist/exchanges/aster-typed-data.js +189 -0
  62. package/dist/exchanges/aster.d.ts +60 -11
  63. package/dist/exchanges/aster.js +278 -136
  64. package/dist/exchanges/hyperliquid.d.ts +14 -0
  65. package/dist/exchanges/hyperliquid.js +88 -34
  66. package/dist/exchanges/lighter-spot.js +19 -5
  67. package/dist/exchanges/lighter.d.ts +66 -0
  68. package/dist/exchanges/lighter.js +213 -31
  69. package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
  70. package/dist/exchanges/pacifica-typed-data.js +74 -0
  71. package/dist/exchanges/pacifica.d.ts +29 -1
  72. package/dist/exchanges/pacifica.js +94 -6
  73. package/dist/funding/rates.js +59 -21
  74. package/dist/guardrail/contracts.d.ts +14 -0
  75. package/dist/guardrail/contracts.js +72 -0
  76. package/dist/guardrail/perp-guardrail.d.ts +21 -0
  77. package/dist/guardrail/perp-guardrail.js +145 -0
  78. package/dist/index.d.ts +1 -1
  79. package/dist/index.js +307 -362
  80. package/dist/jobs.js +3 -1
  81. package/dist/mcp-server.js +92 -88
  82. package/dist/position-history.js +47 -4
  83. package/dist/settings.d.ts +69 -3
  84. package/dist/settings.js +3 -2
  85. package/dist/signer/index.d.ts +2 -0
  86. package/dist/signer/index.js +2 -0
  87. package/dist/signer/ows-evm.d.ts +32 -0
  88. package/dist/signer/ows-evm.js +86 -0
  89. package/dist/signer/ows-loader.d.ts +2 -0
  90. package/dist/signer/ows-loader.js +6 -0
  91. package/dist/signer/ows-solana.d.ts +25 -0
  92. package/dist/signer/ows-solana.js +56 -0
  93. package/dist/smart-order.d.ts +8 -2
  94. package/dist/smart-order.js +46 -11
  95. package/dist/strategies/funding-arb.js +29 -8
  96. package/dist/trade-validator.js +30 -5
  97. package/dist/utils.d.ts +10 -0
  98. package/dist/utils.js +19 -0
  99. package/package.json +4 -2
  100. package/skills/perp-cli/SKILL.md +23 -14
  101. package/skills/perp-cli/references/agent-operations.md +6 -4
  102. package/skills/perp-cli/references/commands.md +55 -34
  103. package/skills/perp-cli/scripts/preflight.sh +4 -4
  104. package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
  105. package/dist/__tests__/alert-logic.test.d.ts +0 -1
  106. package/dist/__tests__/alert-logic.test.js +0 -107
  107. package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
  108. package/dist/__tests__/arb-auto-3dex.test.js +0 -397
  109. package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
  110. package/dist/__tests__/arb-history-stats.test.js +0 -176
  111. package/dist/__tests__/arb-logic.test.d.ts +0 -1
  112. package/dist/__tests__/arb-logic.test.js +0 -84
  113. package/dist/__tests__/arb-manage.test.d.ts +0 -1
  114. package/dist/__tests__/arb-manage.test.js +0 -253
  115. package/dist/__tests__/arb-new-features.test.d.ts +0 -1
  116. package/dist/__tests__/arb-new-features.test.js +0 -457
  117. package/dist/__tests__/arb-sizing.test.d.ts +0 -1
  118. package/dist/__tests__/arb-sizing.test.js +0 -48
  119. package/dist/__tests__/arb-state.test.d.ts +0 -1
  120. package/dist/__tests__/arb-state.test.js +0 -284
  121. package/dist/__tests__/arb-userflow.test.d.ts +0 -1
  122. package/dist/__tests__/arb-userflow.test.js +0 -945
  123. package/dist/__tests__/arb-utils.test.d.ts +0 -1
  124. package/dist/__tests__/arb-utils.test.js +0 -264
  125. package/dist/__tests__/bot-conditions.test.d.ts +0 -1
  126. package/dist/__tests__/bot-conditions.test.js +0 -341
  127. package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
  128. package/dist/__tests__/client-id-tracker.test.js +0 -137
  129. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
  130. package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
  131. package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
  132. package/dist/__tests__/commands/order-intent.test.js +0 -600
  133. package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
  134. package/dist/__tests__/commands/trade-commands.test.js +0 -821
  135. package/dist/__tests__/config.test.d.ts +0 -1
  136. package/dist/__tests__/config.test.js +0 -86
  137. package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
  138. package/dist/__tests__/cross-chain-margin.test.js +0 -287
  139. package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
  140. package/dist/__tests__/dex-asset-map.test.js +0 -191
  141. package/dist/__tests__/errors.test.d.ts +0 -1
  142. package/dist/__tests__/errors.test.js +0 -110
  143. package/dist/__tests__/event-stream.test.d.ts +0 -1
  144. package/dist/__tests__/event-stream.test.js +0 -276
  145. package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
  146. package/dist/__tests__/exchanges/interface.test.js +0 -132
  147. package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
  148. package/dist/__tests__/exchanges/mock-adapter.js +0 -137
  149. package/dist/__tests__/execution-log.test.d.ts +0 -1
  150. package/dist/__tests__/execution-log.test.js +0 -106
  151. package/dist/__tests__/funding-calc.test.d.ts +0 -1
  152. package/dist/__tests__/funding-calc.test.js +0 -71
  153. package/dist/__tests__/funding-history.test.d.ts +0 -1
  154. package/dist/__tests__/funding-history.test.js +0 -343
  155. package/dist/__tests__/funding-rates.test.d.ts +0 -1
  156. package/dist/__tests__/funding-rates.test.js +0 -342
  157. package/dist/__tests__/funding.test.d.ts +0 -1
  158. package/dist/__tests__/funding.test.js +0 -173
  159. package/dist/__tests__/gap-logic.test.d.ts +0 -1
  160. package/dist/__tests__/gap-logic.test.js +0 -43
  161. package/dist/__tests__/hip3-dex.test.d.ts +0 -1
  162. package/dist/__tests__/hip3-dex.test.js +0 -234
  163. package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
  164. package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
  165. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
  166. package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
  167. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
  168. package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
  169. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
  170. package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
  171. package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
  172. package/dist/__tests__/integration/bridge.integration.test.js +0 -309
  173. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
  174. package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
  175. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
  176. package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
  177. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
  178. package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
  179. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
  180. package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
  181. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
  182. package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
  183. package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
  184. package/dist/__tests__/integration/lighter.integration.test.js +0 -53
  185. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
  186. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
  187. package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
  188. package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
  189. package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
  190. package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
  191. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
  192. package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
  193. package/dist/__tests__/liquidity.test.d.ts +0 -1
  194. package/dist/__tests__/liquidity.test.js +0 -225
  195. package/dist/__tests__/plan-executor.test.d.ts +0 -1
  196. package/dist/__tests__/plan-executor.test.js +0 -314
  197. package/dist/__tests__/position-history.test.d.ts +0 -1
  198. package/dist/__tests__/position-history.test.js +0 -367
  199. package/dist/__tests__/retry.test.d.ts +0 -1
  200. package/dist/__tests__/retry.test.js +0 -310
  201. package/dist/__tests__/risk-assessment.test.d.ts +0 -1
  202. package/dist/__tests__/risk-assessment.test.js +0 -288
  203. package/dist/__tests__/security-adversarial.test.d.ts +0 -1
  204. package/dist/__tests__/security-adversarial.test.js +0 -574
  205. package/dist/__tests__/strategies.test.d.ts +0 -1
  206. package/dist/__tests__/strategies.test.js +0 -539
  207. package/dist/__tests__/trade-execution.test.d.ts +0 -1
  208. package/dist/__tests__/trade-execution.test.js +0 -129
  209. package/dist/__tests__/trade-validator.test.d.ts +0 -1
  210. package/dist/__tests__/trade-validator.test.js +0 -655
  211. package/dist/__tests__/utils.test.d.ts +0 -1
  212. package/dist/__tests__/utils.test.js +0 -76
  213. package/dist/arb/reconcile.d.ts +0 -28
  214. package/dist/arb/reconcile.js +0 -162
  215. package/dist/commands/alert.d.ts +0 -2
  216. package/dist/commands/alert.js +0 -421
  217. package/dist/commands/analytics.d.ts +0 -3
  218. package/dist/commands/analytics.js +0 -311
  219. package/dist/commands/arb-manage.d.ts +0 -5
  220. package/dist/commands/arb-manage.js +0 -5
  221. package/dist/commands/dashboard.d.ts +0 -3
  222. package/dist/commands/dashboard.js +0 -176
  223. package/dist/commands/deposit.d.ts +0 -4
  224. package/dist/commands/deposit.js +0 -573
  225. package/dist/commands/dex.d.ts +0 -3
  226. package/dist/commands/dex.js +0 -120
  227. package/dist/commands/env.d.ts +0 -2
  228. package/dist/commands/env.js +0 -136
  229. package/dist/commands/funding.d.ts +0 -3
  230. package/dist/commands/funding.js +0 -554
  231. package/dist/commands/gap.d.ts +0 -2
  232. package/dist/commands/gap.js +0 -305
  233. package/dist/commands/health.d.ts +0 -2
  234. package/dist/commands/health.js +0 -67
  235. package/dist/commands/multileg.d.ts +0 -16
  236. package/dist/commands/multileg.js +0 -174
  237. package/dist/commands/pnl.d.ts +0 -12
  238. package/dist/commands/pnl.js +0 -243
  239. package/dist/commands/run.d.ts +0 -3
  240. package/dist/commands/run.js +0 -202
  241. package/dist/commands/stream.d.ts +0 -5
  242. package/dist/commands/stream.js +0 -200
  243. package/dist/commands/withdraw.d.ts +0 -3
  244. package/dist/commands/withdraw.js +0 -187
  245. package/dist/exchanges/edgex.d.ts +0 -62
  246. package/dist/exchanges/edgex.js +0 -552
  247. package/dist/exchanges/nado.d.ts +0 -52
  248. package/dist/exchanges/nado.js +0 -223
  249. package/dist/funding/index.d.ts +0 -3
  250. package/dist/funding/index.js +0 -3
  251. package/dist/retry.d.ts +0 -74
  252. package/dist/retry.js +0 -129
  253. package/dist/ws/market-feed.d.ts +0 -74
  254. package/dist/ws/market-feed.js +0 -227
@@ -1 +0,0 @@
1
- export {};
@@ -1,176 +0,0 @@
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
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,84 +0,0 @@
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
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,253 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import { existsSync, unlinkSync, readFileSync, writeFileSync, renameSync } from "fs";
3
- import { resolve } from "path";
4
- import { logExecution, readExecutionLog } from "../execution-log.js";
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.arb-manage-backup");
8
- function detectArbPairs(positions) {
9
- const bySymbol = new Map();
10
- for (const p of positions) {
11
- const key = p.symbol.toUpperCase();
12
- if (!bySymbol.has(key))
13
- bySymbol.set(key, []);
14
- bySymbol.get(key).push(p);
15
- }
16
- const pairs = [];
17
- for (const [symbol, pos] of bySymbol) {
18
- const longs = pos.filter(p => p.side === "long");
19
- const shorts = pos.filter(p => p.side === "short");
20
- for (const l of longs) {
21
- for (const s of shorts) {
22
- if (l.exchange !== s.exchange) {
23
- pairs.push({ symbol, longExchange: l.exchange, shortExchange: s.exchange, longPos: l, shortPos: s });
24
- }
25
- }
26
- }
27
- }
28
- return pairs;
29
- }
30
- // ── Tests ──
31
- describe("Arb pair detection", () => {
32
- it("matches positions on different exchanges with opposite sides", () => {
33
- const positions = [
34
- { exchange: "hyperliquid", symbol: "BTC", side: "long", size: 0.1, entryPrice: 100000, markPrice: 101000, unrealizedPnl: 100 },
35
- { exchange: "lighter", symbol: "BTC", side: "short", size: 0.1, entryPrice: 100500, markPrice: 101000, unrealizedPnl: -50 },
36
- ];
37
- const pairs = detectArbPairs(positions);
38
- expect(pairs).toHaveLength(1);
39
- expect(pairs[0].symbol).toBe("BTC");
40
- expect(pairs[0].longExchange).toBe("hyperliquid");
41
- expect(pairs[0].shortExchange).toBe("lighter");
42
- });
43
- it("does not match positions on the same exchange", () => {
44
- const positions = [
45
- { exchange: "hyperliquid", symbol: "ETH", side: "long", size: 1, entryPrice: 3000, markPrice: 3100, unrealizedPnl: 100 },
46
- { exchange: "hyperliquid", symbol: "ETH", side: "short", size: 1, entryPrice: 3050, markPrice: 3100, unrealizedPnl: -50 },
47
- ];
48
- const pairs = detectArbPairs(positions);
49
- expect(pairs).toHaveLength(0);
50
- });
51
- it("does not match positions with same side", () => {
52
- const positions = [
53
- { exchange: "hyperliquid", symbol: "SOL", side: "long", size: 10, entryPrice: 150, markPrice: 155, unrealizedPnl: 50 },
54
- { exchange: "lighter", symbol: "SOL", side: "long", size: 10, entryPrice: 151, markPrice: 155, unrealizedPnl: 40 },
55
- ];
56
- const pairs = detectArbPairs(positions);
57
- expect(pairs).toHaveLength(0);
58
- });
59
- it("returns empty for no positions", () => {
60
- const pairs = detectArbPairs([]);
61
- expect(pairs).toHaveLength(0);
62
- });
63
- it("matches multiple arb pairs for different symbols", () => {
64
- const positions = [
65
- { exchange: "hyperliquid", symbol: "BTC", side: "long", size: 0.1, entryPrice: 100000, markPrice: 101000, unrealizedPnl: 100 },
66
- { exchange: "lighter", symbol: "BTC", side: "short", size: 0.1, entryPrice: 100500, markPrice: 101000, unrealizedPnl: -50 },
67
- { exchange: "pacifica", symbol: "ETH", side: "long", size: 1, entryPrice: 3000, markPrice: 3100, unrealizedPnl: 100 },
68
- { exchange: "hyperliquid", symbol: "ETH", side: "short", size: 1, entryPrice: 3050, markPrice: 3100, unrealizedPnl: -50 },
69
- ];
70
- const pairs = detectArbPairs(positions);
71
- expect(pairs).toHaveLength(2);
72
- const symbols = pairs.map(p => p.symbol).sort();
73
- expect(symbols).toEqual(["BTC", "ETH"]);
74
- });
75
- it("handles case-insensitive symbol matching", () => {
76
- const positions = [
77
- { exchange: "hyperliquid", symbol: "btc", side: "long", size: 0.1, entryPrice: 100000, markPrice: 101000, unrealizedPnl: 100 },
78
- { exchange: "lighter", symbol: "BTC", side: "short", size: 0.1, entryPrice: 100500, markPrice: 101000, unrealizedPnl: -50 },
79
- ];
80
- const pairs = detectArbPairs(positions);
81
- expect(pairs).toHaveLength(1);
82
- });
83
- });
84
- describe("Arb close dry-run", () => {
85
- it("dry-run flag prevents execution", () => {
86
- // Simulating dry-run behavior: the flag should be passed through
87
- const dryRun = true;
88
- const executed = [];
89
- // In dry-run mode, no orders should be placed
90
- if (!dryRun) {
91
- executed.push("sell");
92
- executed.push("buy");
93
- }
94
- expect(executed).toHaveLength(0);
95
- });
96
- it("produces correct actions for close", () => {
97
- const longPos = { exchange: "hyperliquid", symbol: "BTC", rawSymbol: "BTC", side: "long", size: 0.1 };
98
- const shortPos = { exchange: "lighter", symbol: "BTC", rawSymbol: "BTC-PERP", side: "short", size: 0.1 };
99
- const actions = [
100
- { exchange: longPos.exchange, action: "sell", symbol: longPos.rawSymbol, size: String(longPos.size) },
101
- { exchange: shortPos.exchange, action: "buy", symbol: shortPos.rawSymbol, size: String(shortPos.size) },
102
- ];
103
- expect(actions).toHaveLength(2);
104
- expect(actions[0].action).toBe("sell"); // close long = sell
105
- expect(actions[1].action).toBe("buy"); // close short = buy
106
- expect(actions[0].exchange).toBe("hyperliquid");
107
- expect(actions[1].exchange).toBe("lighter");
108
- });
109
- });
110
- describe("Arb history grouping and stats", () => {
111
- beforeEach(() => {
112
- if (existsSync(LOG_FILE)) {
113
- const content = readFileSync(LOG_FILE, "utf-8");
114
- writeFileSync(BACKUP_FILE, content);
115
- }
116
- if (existsSync(LOG_FILE))
117
- unlinkSync(LOG_FILE);
118
- });
119
- afterEach(() => {
120
- if (existsSync(LOG_FILE))
121
- unlinkSync(LOG_FILE);
122
- if (existsSync(BACKUP_FILE)) {
123
- renameSync(BACKUP_FILE, LOG_FILE);
124
- }
125
- });
126
- it("groups arb entries and closes by symbol", () => {
127
- logExecution({
128
- type: "arb_entry", exchange: "hyperliquid+lighter",
129
- symbol: "BTC", side: "entry", size: "0.1",
130
- status: "success", dryRun: false,
131
- meta: { longExchange: "hyperliquid", shortExchange: "lighter", spread: 35, markPrice: 100000 },
132
- });
133
- logExecution({
134
- type: "arb_close", exchange: "hyperliquid+lighter",
135
- symbol: "BTC", side: "close", size: "0.1",
136
- status: "success", dryRun: false,
137
- meta: { longExchange: "hyperliquid", shortExchange: "lighter", currentSpread: 3, unrealizedPnl: 5.5, netPnl: 4.2 },
138
- });
139
- const entries = readExecutionLog({ type: "arb_entry" });
140
- const closes = readExecutionLog({ type: "arb_close" });
141
- expect(entries).toHaveLength(1);
142
- expect(closes).toHaveLength(1);
143
- expect(entries[0].symbol).toBe("BTC");
144
- expect(closes[0].symbol).toBe("BTC");
145
- expect(entries[0].meta?.spread).toBe(35);
146
- expect(closes[0].meta?.netPnl).toBe(4.2);
147
- });
148
- it("calculates trade stats from multiple entries", () => {
149
- // Trade 1: winner
150
- logExecution({
151
- type: "arb_entry", exchange: "hyperliquid+lighter",
152
- symbol: "BTC", side: "entry", size: "0.1",
153
- status: "success", dryRun: false,
154
- meta: { spread: 40, markPrice: 100000 },
155
- });
156
- logExecution({
157
- type: "arb_close", exchange: "hyperliquid+lighter",
158
- symbol: "BTC", side: "close", size: "0.1",
159
- status: "success", dryRun: false,
160
- meta: { unrealizedPnl: 10, netPnl: 8 },
161
- });
162
- // Trade 2: loser
163
- logExecution({
164
- type: "arb_entry", exchange: "pacifica+hyperliquid",
165
- symbol: "ETH", side: "entry", size: "1.0",
166
- status: "success", dryRun: false,
167
- meta: { spread: 25, markPrice: 3000 },
168
- });
169
- logExecution({
170
- type: "arb_close", exchange: "pacifica+hyperliquid",
171
- symbol: "ETH", side: "close", size: "1.0",
172
- status: "success", dryRun: false,
173
- meta: { unrealizedPnl: -5, netPnl: -7 },
174
- });
175
- const allEntries = readExecutionLog()
176
- .filter(r => r.type === "arb_entry" || r.type === "arb_close");
177
- const entries = allEntries.filter(r => r.type === "arb_entry");
178
- const closes = allEntries.filter(r => r.type === "arb_close");
179
- expect(entries).toHaveLength(2);
180
- expect(closes).toHaveLength(2);
181
- // Verify we can compute stats
182
- const completedNetPnls = closes
183
- .filter(c => c.status === "success" && c.meta?.netPnl !== undefined)
184
- .map(c => Number(c.meta.netPnl));
185
- expect(completedNetPnls).toHaveLength(2);
186
- const totalNetPnl = completedNetPnls.reduce((s, n) => s + n, 0);
187
- expect(totalNetPnl).toBe(1); // 8 + (-7) = 1
188
- const winners = completedNetPnls.filter(n => n > 0);
189
- expect(winners).toHaveLength(1);
190
- const winRate = (winners.length / completedNetPnls.length) * 100;
191
- expect(winRate).toBe(50);
192
- });
193
- it("handles entries without matching closes (open trades)", () => {
194
- logExecution({
195
- type: "arb_entry", exchange: "hyperliquid+lighter",
196
- symbol: "SOL", side: "entry", size: "10",
197
- status: "success", dryRun: false,
198
- meta: { spread: 30, markPrice: 150 },
199
- });
200
- const entries = readExecutionLog({ type: "arb_entry" });
201
- const closes = readExecutionLog({ type: "arb_close" });
202
- expect(entries).toHaveLength(1);
203
- expect(closes).toHaveLength(0);
204
- // Should be treated as an open trade
205
- const symbol = entries[0].symbol;
206
- const matchingClose = closes.find(c => c.symbol === symbol);
207
- expect(matchingClose).toBeUndefined();
208
- });
209
- it("filters by period correctly", () => {
210
- // Log an entry with a very old timestamp manually
211
- const oldDate = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString();
212
- const recentDate = new Date().toISOString();
213
- logExecution({
214
- type: "arb_entry", exchange: "hyperliquid+lighter",
215
- symbol: "OLD_TRADE", side: "entry", size: "0.1",
216
- status: "success", dryRun: false,
217
- });
218
- logExecution({
219
- type: "arb_entry", exchange: "hyperliquid+lighter",
220
- symbol: "NEW_TRADE", side: "entry", size: "0.1",
221
- status: "success", dryRun: false,
222
- });
223
- // Read with 30-day filter: both are recent (logged just now), so both should appear
224
- const since30d = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
225
- const recent = readExecutionLog({ type: "arb_entry", since: since30d });
226
- expect(recent).toHaveLength(2);
227
- });
228
- });
229
- describe("Duration formatting", () => {
230
- function formatDuration(ms) {
231
- const hours = Math.floor(ms / (1000 * 60 * 60));
232
- const days = Math.floor(hours / 24);
233
- const remainingHours = hours % 24;
234
- if (days > 0)
235
- return `${days}d ${remainingHours}h`;
236
- if (hours > 0)
237
- return `${hours}h`;
238
- const minutes = Math.floor(ms / (1000 * 60));
239
- return `${minutes}m`;
240
- }
241
- it("formats minutes", () => {
242
- expect(formatDuration(30 * 60 * 1000)).toBe("30m");
243
- });
244
- it("formats hours", () => {
245
- expect(formatDuration(5 * 60 * 60 * 1000)).toBe("5h");
246
- });
247
- it("formats days and hours", () => {
248
- expect(formatDuration(26 * 60 * 60 * 1000)).toBe("1d 2h");
249
- });
250
- it("formats zero", () => {
251
- expect(formatDuration(0)).toBe("0m");
252
- });
253
- });
@@ -1 +0,0 @@
1
- export {};