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,457 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { existsSync, unlinkSync, mkdirSync } from "fs";
3
- import { resolve } from "path";
4
- // ── Imports from actual source files ──
5
- import { formatNotifyMessage, notifyIfEnabled, } from "../arb-utils.js";
6
- import { loadArbState, saveArbState, createInitialState, setStateFilePath, resetStateFilePath, } from "../arb-state.js";
7
- import { computeNetSpread, computeRoundTripCostPct, isNearSettlement, isSpreadReversed, getNextSettlement, } from "../commands/arb-auto.js";
8
- // ══════════════════════════════════════════════════════════════════════════════
9
- // 1. Promise.allSettled rollback logic (mock-based)
10
- // ══════════════════════════════════════════════════════════════════════════════
11
- describe("Promise.allSettled rollback logic", () => {
12
- /**
13
- * Re-implements the decision logic from arb-auto.ts daemon cycle:
14
- * After calling Promise.allSettled on [longOrder, shortOrder], determine
15
- * what action to take based on the settlement results.
16
- */
17
- function evaluateDualLegResult(longResult, shortResult) {
18
- const longOk = longResult.status === "fulfilled";
19
- const shortOk = shortResult.status === "fulfilled";
20
- if (longOk && shortOk) {
21
- return { action: "success" };
22
- }
23
- else if (longOk !== shortOk) {
24
- const filledSide = longOk ? "long" : "short";
25
- const failedSide = longOk ? "short" : "long";
26
- return { action: "rollback", filledSide, failedSide };
27
- }
28
- else {
29
- return { action: "both_failed" };
30
- }
31
- }
32
- it("both fulfilled -> indicates success", () => {
33
- const longResult = { status: "fulfilled", value: "order-123" };
34
- const shortResult = { status: "fulfilled", value: "order-456" };
35
- const result = evaluateDualLegResult(longResult, shortResult);
36
- expect(result.action).toBe("success");
37
- expect(result.filledSide).toBeUndefined();
38
- expect(result.failedSide).toBeUndefined();
39
- });
40
- it("long fulfilled, short rejected -> should attempt rollback of long", () => {
41
- const longResult = { status: "fulfilled", value: "order-123" };
42
- const shortResult = { status: "rejected", reason: new Error("insufficient margin") };
43
- const result = evaluateDualLegResult(longResult, shortResult);
44
- expect(result.action).toBe("rollback");
45
- expect(result.filledSide).toBe("long");
46
- expect(result.failedSide).toBe("short");
47
- });
48
- it("short fulfilled, long rejected -> should attempt rollback of short", () => {
49
- const longResult = { status: "rejected", reason: new Error("timeout") };
50
- const shortResult = { status: "fulfilled", value: "order-789" };
51
- const result = evaluateDualLegResult(longResult, shortResult);
52
- expect(result.action).toBe("rollback");
53
- expect(result.filledSide).toBe("short");
54
- expect(result.failedSide).toBe("long");
55
- });
56
- it("both rejected -> indicates both failed, no rollback needed", () => {
57
- const longResult = { status: "rejected", reason: new Error("exchange down") };
58
- const shortResult = { status: "rejected", reason: new Error("rate limited") };
59
- const result = evaluateDualLegResult(longResult, shortResult);
60
- expect(result.action).toBe("both_failed");
61
- expect(result.filledSide).toBeUndefined();
62
- expect(result.failedSide).toBeUndefined();
63
- });
64
- it("rollback action determines correct reverse order direction", () => {
65
- // In arb-auto.ts: rollbackAction = longOk ? "sell" : "buy"
66
- const longOk = true;
67
- const shortOk = false;
68
- const rollbackAction = longOk ? "sell" : "buy";
69
- expect(rollbackAction).toBe("sell"); // reverse the long (buy) with a sell
70
- });
71
- });
72
- // ══════════════════════════════════════════════════════════════════════════════
73
- // 2. --min-size threshold
74
- // ══════════════════════════════════════════════════════════════════════════════
75
- describe("--min-size threshold", () => {
76
- /**
77
- * Re-implements the min-size gate from arb-auto.ts:
78
- * if (sizeIsAuto && actualSizeUsd < minSizeUsd) { continue; }
79
- */
80
- function shouldSkipForMinSize(sizeIsAuto, actualSizeUsd, minSizeUsd) {
81
- if (sizeIsAuto && actualSizeUsd < minSizeUsd)
82
- return true;
83
- return false;
84
- }
85
- it("auto-size $25 with min-size $30 -> should skip (below floor)", () => {
86
- expect(shouldSkipForMinSize(true, 25, 30)).toBe(true);
87
- });
88
- it("auto-size $50 with min-size $30 -> should proceed", () => {
89
- expect(shouldSkipForMinSize(true, 50, 30)).toBe(false);
90
- });
91
- it("auto-size $30 with min-size $30 -> should proceed (exact boundary)", () => {
92
- expect(shouldSkipForMinSize(true, 30, 30)).toBe(false);
93
- });
94
- it("non-auto mode ignores min-size even when size is below threshold", () => {
95
- // When sizeIsAuto is false (fixed size mode), min-size check is skipped
96
- expect(shouldSkipForMinSize(false, 25, 30)).toBe(false);
97
- expect(shouldSkipForMinSize(false, 10, 30)).toBe(false);
98
- });
99
- it("auto-size $0 with any min-size -> should skip", () => {
100
- expect(shouldSkipForMinSize(true, 0, 30)).toBe(true);
101
- });
102
- });
103
- // ══════════════════════════════════════════════════════════════════════════════
104
- // 3. Heartbeat notification
105
- // ══════════════════════════════════════════════════════════════════════════════
106
- describe("Heartbeat notification", () => {
107
- it("formatNotifyMessage('heartbeat') returns proper message with minutes and last scan time", () => {
108
- const msg = formatNotifyMessage("heartbeat", {
109
- lastScanTime: "2025-01-15T10:30:00.000Z",
110
- minutesAgo: 12,
111
- });
112
- expect(msg).toContain("HEARTBEAT");
113
- expect(msg).toContain("12");
114
- expect(msg).toContain("2025-01-15T10:30:00.000Z");
115
- });
116
- it("heartbeat message includes 'minutes' wording", () => {
117
- const msg = formatNotifyMessage("heartbeat", {
118
- lastScanTime: "2025-01-15T08:00:00.000Z",
119
- minutesAgo: 7,
120
- });
121
- expect(msg).toContain("minutes");
122
- expect(msg).toContain("7");
123
- });
124
- it("notifyIfEnabled with 'heartbeat' in events -> sends notification", async () => {
125
- const mockFetch = vi.fn().mockResolvedValue({ ok: true });
126
- const events = ["entry", "exit", "heartbeat"];
127
- await notifyIfEnabled("https://discord.com/api/webhooks/123/abc", events, "heartbeat", { lastScanTime: "2025-01-15T10:00:00Z", minutesAgo: 10 }, mockFetch);
128
- expect(mockFetch).toHaveBeenCalledOnce();
129
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
130
- expect(body.content).toContain("HEARTBEAT");
131
- });
132
- it("notifyIfEnabled with 'heartbeat' NOT in events -> does not send", async () => {
133
- const mockFetch = vi.fn().mockResolvedValue({ ok: true });
134
- const events = ["entry", "exit", "reversal"];
135
- await notifyIfEnabled("https://discord.com/api/webhooks/123/abc", events, "heartbeat", { lastScanTime: "2025-01-15T10:00:00Z", minutesAgo: 10 }, mockFetch);
136
- expect(mockFetch).not.toHaveBeenCalled();
137
- });
138
- it("notifyIfEnabled with empty events list -> sends (empty = all events)", async () => {
139
- const mockFetch = vi.fn().mockResolvedValue({ ok: true });
140
- // Empty events list means "all events enabled"
141
- await notifyIfEnabled("https://discord.com/api/webhooks/123/abc", [], "heartbeat", { lastScanTime: "2025-01-15T10:00:00Z", minutesAgo: 5 }, mockFetch);
142
- expect(mockFetch).toHaveBeenCalledOnce();
143
- });
144
- });
145
- // ══════════════════════════════════════════════════════════════════════════════
146
- // 4. Exit reason tagging
147
- // ══════════════════════════════════════════════════════════════════════════════
148
- describe("Exit reason tagging", () => {
149
- /**
150
- * Re-implements the exit reason derivation from arb-auto.ts:
151
- * const exitReason = closeReason.includes("REVERSAL") ? "reversal"
152
- * : closeReason.includes("spread") ? "spread"
153
- * : "manual";
154
- */
155
- function deriveExitReason(closeReason) {
156
- if (closeReason.includes("REVERSAL"))
157
- return "reversal";
158
- if (closeReason.includes("spread"))
159
- return "spread";
160
- return "manual";
161
- }
162
- it("closeReason containing 'REVERSAL' -> exitReason = 'reversal'", () => {
163
- const reason = "REVERSAL DETECTED — long exchange now has higher rate than short";
164
- expect(deriveExitReason(reason)).toBe("reversal");
165
- });
166
- it("closeReason containing 'spread' -> exitReason = 'spread'", () => {
167
- const reason = "spread 3.2% <= 5%";
168
- expect(deriveExitReason(reason)).toBe("spread");
169
- });
170
- it("manual close -> exitReason = 'manual'", () => {
171
- const reason = "user requested close";
172
- expect(deriveExitReason(reason)).toBe("manual");
173
- });
174
- it("empty closeReason -> exitReason = 'manual'", () => {
175
- expect(deriveExitReason("")).toBe("manual");
176
- });
177
- it("exitReason appears in execution log meta structure", () => {
178
- // Verify the expected shape of an arb_close execution record with exitReason
179
- const record = {
180
- type: "arb_close",
181
- exchange: "hyperliquid+pacifica",
182
- symbol: "ETH",
183
- side: "close",
184
- size: "0.5000",
185
- status: "success",
186
- dryRun: false,
187
- meta: {
188
- longExchange: "hyperliquid",
189
- shortExchange: "pacifica",
190
- currentSpread: 3.2,
191
- reason: "spread 3.2% <= 5%",
192
- exitReason: "spread",
193
- },
194
- };
195
- expect(record.meta).toBeDefined();
196
- expect(record.meta.exitReason).toBe("spread");
197
- });
198
- it("REVERSAL in closeReason takes priority over 'spread' substring", () => {
199
- // If somehow both words appear, REVERSAL check is first
200
- const reason = "REVERSAL — spread has flipped";
201
- expect(deriveExitReason(reason)).toBe("reversal");
202
- });
203
- });
204
- // ══════════════════════════════════════════════════════════════════════════════
205
- // 5. Funding reconciliation
206
- // ══════════════════════════════════════════════════════════════════════════════
207
- describe("Funding reconciliation", () => {
208
- /**
209
- * Re-implements the diff logic from arb-auto.ts arb status command:
210
- * const fundingDiff = totalActualFunding - totalEstFunding;
211
- * const diffPct = totalEstFunding !== 0
212
- * ? ((fundingDiff / Math.abs(totalEstFunding)) * 100).toFixed(1)
213
- * : "N/A";
214
- */
215
- function computeFundingReconciliation(estimated, actual) {
216
- const diff = actual - estimated;
217
- const diffPct = estimated !== 0
218
- ? ((diff / Math.abs(estimated)) * 100).toFixed(1)
219
- : "N/A";
220
- return { diff, diffPct };
221
- }
222
- it("computes positive diff when actual > estimated", () => {
223
- const result = computeFundingReconciliation(10, 12);
224
- expect(result.diff).toBe(2);
225
- expect(result.diffPct).toBe("20.0");
226
- });
227
- it("computes negative diff when actual < estimated", () => {
228
- const result = computeFundingReconciliation(10, 8);
229
- expect(result.diff).toBe(-2);
230
- expect(result.diffPct).toBe("-20.0");
231
- });
232
- it("zero diff when actual = estimated", () => {
233
- const result = computeFundingReconciliation(5, 5);
234
- expect(result.diff).toBe(0);
235
- expect(result.diffPct).toBe("0.0");
236
- });
237
- it("handles when estimated = 0 (returns N/A for percentage)", () => {
238
- const result = computeFundingReconciliation(0, 3.5);
239
- expect(result.diff).toBe(3.5);
240
- expect(result.diffPct).toBe("N/A");
241
- });
242
- it("handles when actual = 0 (no settled funding yet)", () => {
243
- const result = computeFundingReconciliation(5, 0);
244
- expect(result.diff).toBe(-5);
245
- expect(result.diffPct).toBe("-100.0");
246
- });
247
- it("handles negative estimated funding", () => {
248
- const result = computeFundingReconciliation(-10, -12);
249
- expect(result.diff).toBe(-2);
250
- // diff / |estimated| = -2/10 = -20%
251
- expect(result.diffPct).toBe("-20.0");
252
- });
253
- });
254
- // ══════════════════════════════════════════════════════════════════════════════
255
- // 6. ArbDaemonState with lastSuccessfulScanTime
256
- // ══════════════════════════════════════════════════════════════════════════════
257
- describe("ArbDaemonState with lastSuccessfulScanTime", () => {
258
- const TEST_DIR = resolve(process.env.HOME || "~", ".perp", "test-arb-new-features");
259
- const TEST_FILE = resolve(TEST_DIR, "arb-state.json");
260
- const TMP_FILE = TEST_FILE + ".tmp";
261
- beforeEach(() => {
262
- if (!existsSync(TEST_DIR))
263
- mkdirSync(TEST_DIR, { recursive: true });
264
- if (existsSync(TEST_FILE))
265
- unlinkSync(TEST_FILE);
266
- if (existsSync(TMP_FILE))
267
- unlinkSync(TMP_FILE);
268
- setStateFilePath(TEST_FILE);
269
- });
270
- afterEach(() => {
271
- if (existsSync(TEST_FILE))
272
- unlinkSync(TEST_FILE);
273
- if (existsSync(TMP_FILE))
274
- unlinkSync(TMP_FILE);
275
- resetStateFilePath();
276
- });
277
- it("createInitialState includes lastSuccessfulScanTime", () => {
278
- const state = createInitialState({
279
- minSpread: 30, closeSpread: 5, size: 100,
280
- holdDays: 7, bridgeCost: 0.5, maxPositions: 5, settleStrategy: "block",
281
- });
282
- expect(state).toHaveProperty("lastSuccessfulScanTime");
283
- expect(state.lastSuccessfulScanTime).toBeTruthy();
284
- });
285
- it("lastSuccessfulScanTime is an ISO timestamp string", () => {
286
- const state = createInitialState({
287
- minSpread: 30, closeSpread: 5, size: 100,
288
- holdDays: 7, bridgeCost: 0.5, maxPositions: 5, settleStrategy: "block",
289
- });
290
- // Validate ISO format by parsing it
291
- const parsed = new Date(state.lastSuccessfulScanTime);
292
- expect(parsed.getTime()).not.toBeNaN();
293
- expect(state.lastSuccessfulScanTime).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
294
- });
295
- it("saveArbState + loadArbState preserves lastSuccessfulScanTime", () => {
296
- const state = createInitialState({
297
- minSpread: 30, closeSpread: 5, size: 100,
298
- holdDays: 7, bridgeCost: 0.5, maxPositions: 5, settleStrategy: "block",
299
- });
300
- const testTimestamp = "2025-06-15T14:30:00.000Z";
301
- state.lastSuccessfulScanTime = testTimestamp;
302
- saveArbState(state);
303
- const loaded = loadArbState();
304
- expect(loaded).not.toBeNull();
305
- expect(loaded.lastSuccessfulScanTime).toBe(testTimestamp);
306
- });
307
- it("lastSuccessfulScanTime survives state update cycle", () => {
308
- const state = createInitialState({
309
- minSpread: 30, closeSpread: 5, size: 100,
310
- holdDays: 7, bridgeCost: 0.5, maxPositions: 5, settleStrategy: "block",
311
- });
312
- const ts1 = "2025-06-15T10:00:00.000Z";
313
- state.lastSuccessfulScanTime = ts1;
314
- saveArbState(state);
315
- // Simulate daemon updating lastSuccessfulScanTime
316
- const loaded = loadArbState();
317
- const ts2 = "2025-06-15T11:00:00.000Z";
318
- loaded.lastSuccessfulScanTime = ts2;
319
- loaded.lastScanTime = ts2;
320
- saveArbState(loaded);
321
- const reloaded = loadArbState();
322
- expect(reloaded.lastSuccessfulScanTime).toBe(ts2);
323
- expect(reloaded.lastScanTime).toBe(ts2);
324
- });
325
- });
326
- // ══════════════════════════════════════════════════════════════════════════════
327
- // 7. Exchange downtime detection logic
328
- // ══════════════════════════════════════════════════════════════════════════════
329
- describe("Exchange downtime detection logic", () => {
330
- /**
331
- * Re-implements the exchange health check pattern from arb-auto.ts:
332
- * - Try getMarkets() for each exchange
333
- * - If it throws, add to downExchanges and blockedExchanges
334
- * - If a position has an exchange in downExchanges, mark as degraded
335
- */
336
- async function checkExchangeHealth(exchangeNames, getMarketsFn) {
337
- const downExchanges = new Set();
338
- const blockedExchanges = new Set();
339
- for (const name of exchangeNames) {
340
- try {
341
- await getMarketsFn(name);
342
- }
343
- catch {
344
- downExchanges.add(name);
345
- blockedExchanges.add(name);
346
- }
347
- }
348
- return { downExchanges, blockedExchanges };
349
- }
350
- function isPositionDegraded(position, downExchanges) {
351
- return downExchanges.has(position.longExchange) || downExchanges.has(position.shortExchange);
352
- }
353
- it("getMarkets() success -> not in downExchanges", async () => {
354
- const getMarkets = vi.fn().mockResolvedValue([{ symbol: "BTC", markPrice: "50000" }]);
355
- const result = await checkExchangeHealth(["hyperliquid"], getMarkets);
356
- expect(result.downExchanges.has("hyperliquid")).toBe(false);
357
- expect(result.blockedExchanges.has("hyperliquid")).toBe(false);
358
- });
359
- it("getMarkets() throws -> in downExchanges and blockedExchanges", async () => {
360
- const getMarkets = vi.fn().mockRejectedValue(new Error("connection refused"));
361
- const result = await checkExchangeHealth(["lighter"], getMarkets);
362
- expect(result.downExchanges.has("lighter")).toBe(true);
363
- expect(result.blockedExchanges.has("lighter")).toBe(true);
364
- });
365
- it("mixed exchange health: some up, some down", async () => {
366
- const getMarkets = vi.fn()
367
- .mockImplementation(async (name) => {
368
- if (name === "pacifica")
369
- throw new Error("503 service unavailable");
370
- return [{ symbol: "ETH" }];
371
- });
372
- const result = await checkExchangeHealth(["hyperliquid", "pacifica", "lighter"], getMarkets);
373
- expect(result.downExchanges.has("pacifica")).toBe(true);
374
- expect(result.blockedExchanges.has("pacifica")).toBe(true);
375
- expect(result.downExchanges.has("hyperliquid")).toBe(false);
376
- expect(result.downExchanges.has("lighter")).toBe(false);
377
- });
378
- it("position on down exchange -> marked as degraded", async () => {
379
- const downExchanges = new Set(["pacifica"]);
380
- const position = { longExchange: "hyperliquid", shortExchange: "pacifica" };
381
- expect(isPositionDegraded(position, downExchanges)).toBe(true);
382
- });
383
- it("position on healthy exchanges -> not degraded", () => {
384
- const downExchanges = new Set();
385
- const position = { longExchange: "hyperliquid", shortExchange: "lighter" };
386
- expect(isPositionDegraded(position, downExchanges)).toBe(false);
387
- });
388
- it("position degraded when long exchange is down", () => {
389
- const downExchanges = new Set(["hyperliquid"]);
390
- const position = { longExchange: "hyperliquid", shortExchange: "pacifica" };
391
- expect(isPositionDegraded(position, downExchanges)).toBe(true);
392
- });
393
- });
394
- // ══════════════════════════════════════════════════════════════════════════════
395
- // Additional: computeNetSpread, computeRoundTripCostPct, isNearSettlement
396
- // ══════════════════════════════════════════════════════════════════════════════
397
- describe("computeNetSpread", () => {
398
- it("deducts round-trip cost annualized over hold period", () => {
399
- // Gross 50% annual, 0.38% round trip cost, 7 day hold, no bridge
400
- const net = computeNetSpread(50, 7, 0.38, 0, 0);
401
- // annualized cost = (0.38 / 7) * 365 = ~19.81%
402
- // net = 50 - 19.81 = ~30.19%
403
- expect(net).toBeCloseTo(30.19, 0);
404
- });
405
- it("includes bridge cost when provided", () => {
406
- // Gross 50%, 0.38% RT, 7d hold, $1 bridge, $100 position
407
- const net = computeNetSpread(50, 7, 0.38, 1, 100);
408
- // bridge round-trip pct = (1*2/100)*100 = 2%
409
- // bridge annualized = (2/7)*365 = ~104.3%
410
- // net = 50 - 19.81 - 104.3 -> very negative
411
- expect(net).toBeLessThan(0);
412
- });
413
- });
414
- describe("computeRoundTripCostPct", () => {
415
- it("computes standard costs for default exchanges", () => {
416
- const cost = computeRoundTripCostPct("hyperliquid", "pacifica", 0.05);
417
- // 2 * (0.035 + 0.035) + 2 * 0.05 = 0.14 + 0.10 = 0.24%
418
- expect(cost).toBeCloseTo(0.24, 2);
419
- });
420
- });
421
- describe("isNearSettlement", () => {
422
- it("blocks when within buffer of next settlement", () => {
423
- // 15:57 UTC, next settlement at 16:00 -> 3 minutes away, within 5 min buffer
424
- const now = new Date("2024-06-15T15:57:00Z");
425
- const result = isNearSettlement("hyperliquid", "pacifica", 5, now);
426
- expect(result.blocked).toBe(true);
427
- expect(result.minutesUntil).toBeLessThanOrEqual(5);
428
- });
429
- it("does not block when far from settlement", () => {
430
- // 15:30 UTC, next settlement at 16:00 -> 30 minutes away
431
- const now = new Date("2024-06-15T15:30:00Z");
432
- const result = isNearSettlement("hyperliquid", "pacifica", 5, now);
433
- expect(result.blocked).toBe(false);
434
- });
435
- });
436
- describe("getNextSettlement", () => {
437
- it("returns next hour for hourly exchanges", () => {
438
- const now = new Date("2024-06-15T14:30:00Z");
439
- const next = getNextSettlement("hyperliquid", now);
440
- expect(next.getUTCHours()).toBe(15);
441
- expect(next.getUTCMinutes()).toBe(0);
442
- });
443
- });
444
- describe("isSpreadReversed", () => {
445
- it("detects reversal when long exchange has higher rate", () => {
446
- // Long HL, Short PAC. If HL rate > PAC rate hourly -> reversed
447
- const snapshot = { symbol: "ETH", pacRate: 0.0001, hlRate: 0.0005, ltRate: 0, spread: 0, longExch: "hyperliquid", shortExch: "pacifica", markPrice: 3000, pacMarkPrice: 0, hlMarkPrice: 0, ltMarkPrice: 0 };
448
- const reversed = isSpreadReversed("hyperliquid", "pacifica", snapshot);
449
- expect(reversed).toBe(true);
450
- });
451
- it("no reversal when short exchange has higher rate", () => {
452
- // Long HL, Short PAC. PAC rate > HL rate -> NOT reversed
453
- const snapshot = { symbol: "ETH", pacRate: 0.0005, hlRate: 0.0001, ltRate: 0, spread: 0, longExch: "hyperliquid", shortExch: "pacifica", markPrice: 3000, pacMarkPrice: 0, hlMarkPrice: 0, ltMarkPrice: 0 };
454
- const reversed = isSpreadReversed("hyperliquid", "pacifica", snapshot);
455
- expect(reversed).toBe(false);
456
- });
457
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,48 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { computeMatchedSize } from "../arb-sizing.js";
3
- describe("computeMatchedSize", () => {
4
- it("should compute size respecting least precise exchange", () => {
5
- // HL (1 decimal) + Lighter (2 decimals) -> use 1 decimal
6
- const result = computeMatchedSize(100, 50, "hyperliquid", "lighter");
7
- expect(result).not.toBeNull();
8
- expect(result.size).toBe("2.0"); // 100/50 = 2.0
9
- expect(result.notional).toBe(100);
10
- });
11
- it("should round down to avoid exceeding requested size", () => {
12
- const result = computeMatchedSize(100, 33, "hyperliquid", "lighter");
13
- expect(result).not.toBeNull();
14
- // 100/33 = 3.0303... -> floor to 3.0 (HL=1 decimal)
15
- expect(result.size).toBe("3.0");
16
- expect(result.notional).toBeLessThanOrEqual(100);
17
- });
18
- it("should return null when price is 0", () => {
19
- expect(computeMatchedSize(100, 0, "hyperliquid", "lighter")).toBeNull();
20
- });
21
- it("should return null when size is too small for min notional", () => {
22
- // $5 / $100000 = 0.00005 -> rounds to 0.0 for HL (1 decimal)
23
- expect(computeMatchedSize(5, 100000, "hyperliquid", "lighter")).toBeNull();
24
- });
25
- it("should use more precision for pacifica", () => {
26
- // Pacifica (4 decimals) + Lighter (2 decimals) -> use 2 decimals
27
- const result = computeMatchedSize(100, 3500, "pacifica", "lighter");
28
- expect(result).not.toBeNull();
29
- // 100/3500 = 0.02857... -> floor to 0.02
30
- expect(result.size).toBe("0.02");
31
- });
32
- it("should handle same exchange pair", () => {
33
- const result = computeMatchedSize(1000, 100, "hyperliquid", "hyperliquid");
34
- expect(result).not.toBeNull();
35
- expect(result.size).toBe("10.0");
36
- });
37
- it("should try rounding up if floor is below min notional", () => {
38
- // $12 / $3500 = 0.00342... -> floor to 0.0 (HL 1 decimal) -> try round up to 0.1
39
- // 0.1 * 3500 = 350 which is way more than 12*1.2=14.4, so should return null
40
- expect(computeMatchedSize(12, 3500, "hyperliquid", "lighter")).toBeNull();
41
- });
42
- it("should meet min notional of both exchanges", () => {
43
- // Both HL and LT need $10 min
44
- const result = computeMatchedSize(15, 150, "hyperliquid", "lighter");
45
- expect(result).not.toBeNull();
46
- expect(result.notional).toBeGreaterThanOrEqual(10);
47
- });
48
- });
@@ -1 +0,0 @@
1
- export {};