perp-cli 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -0
  3. package/dist/__tests__/alert-logic.test.d.ts +1 -0
  4. package/dist/__tests__/alert-logic.test.js +107 -0
  5. package/dist/__tests__/arb-auto-3dex.test.d.ts +1 -0
  6. package/dist/__tests__/arb-auto-3dex.test.js +397 -0
  7. package/dist/__tests__/arb-history-stats.test.d.ts +1 -0
  8. package/dist/__tests__/arb-history-stats.test.js +176 -0
  9. package/dist/__tests__/arb-logic.test.d.ts +1 -0
  10. package/dist/__tests__/arb-logic.test.js +84 -0
  11. package/dist/__tests__/arb-manage.test.d.ts +1 -0
  12. package/dist/__tests__/arb-manage.test.js +253 -0
  13. package/dist/__tests__/arb-new-features.test.d.ts +1 -0
  14. package/dist/__tests__/arb-new-features.test.js +457 -0
  15. package/dist/__tests__/arb-sizing.test.d.ts +1 -0
  16. package/dist/__tests__/arb-sizing.test.js +48 -0
  17. package/dist/__tests__/arb-state.test.d.ts +1 -0
  18. package/dist/__tests__/arb-state.test.js +284 -0
  19. package/dist/__tests__/arb-userflow.test.d.ts +1 -0
  20. package/dist/__tests__/arb-userflow.test.js +945 -0
  21. package/dist/__tests__/arb-utils.test.d.ts +1 -0
  22. package/dist/__tests__/arb-utils.test.js +264 -0
  23. package/dist/__tests__/bot-conditions.test.d.ts +1 -0
  24. package/dist/__tests__/bot-conditions.test.js +341 -0
  25. package/dist/__tests__/client-id-tracker.test.d.ts +1 -0
  26. package/dist/__tests__/client-id-tracker.test.js +137 -0
  27. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +1 -0
  28. package/dist/__tests__/commands/new-atomic-commands.test.js +502 -0
  29. package/dist/__tests__/commands/order-intent.test.d.ts +1 -0
  30. package/dist/__tests__/commands/order-intent.test.js +600 -0
  31. package/dist/__tests__/commands/trade-commands.test.d.ts +1 -0
  32. package/dist/__tests__/commands/trade-commands.test.js +821 -0
  33. package/dist/__tests__/config.test.d.ts +1 -0
  34. package/dist/__tests__/config.test.js +86 -0
  35. package/dist/__tests__/cross-chain-margin.test.d.ts +1 -0
  36. package/dist/__tests__/cross-chain-margin.test.js +287 -0
  37. package/dist/__tests__/dex-asset-map.test.d.ts +1 -0
  38. package/dist/__tests__/dex-asset-map.test.js +191 -0
  39. package/dist/__tests__/errors.test.d.ts +1 -0
  40. package/dist/__tests__/errors.test.js +110 -0
  41. package/dist/__tests__/event-stream.test.d.ts +1 -0
  42. package/dist/__tests__/event-stream.test.js +276 -0
  43. package/dist/__tests__/exchanges/interface.test.d.ts +1 -0
  44. package/dist/__tests__/exchanges/interface.test.js +132 -0
  45. package/dist/__tests__/exchanges/mock-adapter.d.ts +69 -0
  46. package/dist/__tests__/exchanges/mock-adapter.js +137 -0
  47. package/dist/__tests__/execution-log.test.d.ts +1 -0
  48. package/dist/__tests__/execution-log.test.js +106 -0
  49. package/dist/__tests__/funding-calc.test.d.ts +1 -0
  50. package/dist/__tests__/funding-calc.test.js +71 -0
  51. package/dist/__tests__/funding-history.test.d.ts +1 -0
  52. package/dist/__tests__/funding-history.test.js +343 -0
  53. package/dist/__tests__/funding-rates.test.d.ts +1 -0
  54. package/dist/__tests__/funding-rates.test.js +342 -0
  55. package/dist/__tests__/funding.test.d.ts +1 -0
  56. package/dist/__tests__/funding.test.js +173 -0
  57. package/dist/__tests__/gap-logic.test.d.ts +1 -0
  58. package/dist/__tests__/gap-logic.test.js +43 -0
  59. package/dist/__tests__/hip3-dex.test.d.ts +1 -0
  60. package/dist/__tests__/hip3-dex.test.js +234 -0
  61. package/dist/__tests__/integration/agent-features.integration.test.d.ts +1 -0
  62. package/dist/__tests__/integration/agent-features.integration.test.js +553 -0
  63. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +13 -0
  64. package/dist/__tests__/integration/atomic-commands.integration.test.js +246 -0
  65. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +1 -0
  66. package/dist/__tests__/integration/bridge-simulation.integration.test.js +453 -0
  67. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +1 -0
  68. package/dist/__tests__/integration/bridge-strict.integration.test.js +812 -0
  69. package/dist/__tests__/integration/bridge.integration.test.d.ts +1 -0
  70. package/dist/__tests__/integration/bridge.integration.test.js +309 -0
  71. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +1 -0
  72. package/dist/__tests__/integration/cli-e2e.integration.test.js +202 -0
  73. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +1 -0
  74. package/dist/__tests__/integration/dex-arb.integration.test.js +116 -0
  75. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +13 -0
  76. package/dist/__tests__/integration/envelope-consistency.integration.test.js +205 -0
  77. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +1 -0
  78. package/dist/__tests__/integration/hip3-dex.integration.test.js +147 -0
  79. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +1 -0
  80. package/dist/__tests__/integration/hyperliquid.integration.test.js +79 -0
  81. package/dist/__tests__/integration/lighter.integration.test.d.ts +1 -0
  82. package/dist/__tests__/integration/lighter.integration.test.js +53 -0
  83. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +9 -0
  84. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +236 -0
  85. package/dist/__tests__/integration/order-verification.integration.test.d.ts +1 -0
  86. package/dist/__tests__/integration/order-verification.integration.test.js +321 -0
  87. package/dist/__tests__/integration/pacifica.integration.test.d.ts +1 -0
  88. package/dist/__tests__/integration/pacifica.integration.test.js +75 -0
  89. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +1 -0
  90. package/dist/__tests__/integration/response-shapes.integration.test.js +278 -0
  91. package/dist/__tests__/liquidity.test.d.ts +1 -0
  92. package/dist/__tests__/liquidity.test.js +225 -0
  93. package/dist/__tests__/plan-executor.test.d.ts +1 -0
  94. package/dist/__tests__/plan-executor.test.js +314 -0
  95. package/dist/__tests__/position-history.test.d.ts +1 -0
  96. package/dist/__tests__/position-history.test.js +367 -0
  97. package/dist/__tests__/retry.test.d.ts +1 -0
  98. package/dist/__tests__/retry.test.js +310 -0
  99. package/dist/__tests__/risk-assessment.test.d.ts +1 -0
  100. package/dist/__tests__/risk-assessment.test.js +145 -0
  101. package/dist/__tests__/security-adversarial.test.d.ts +1 -0
  102. package/dist/__tests__/security-adversarial.test.js +574 -0
  103. package/dist/__tests__/strategies.test.d.ts +1 -0
  104. package/dist/__tests__/strategies.test.js +539 -0
  105. package/dist/__tests__/trade-execution.test.d.ts +1 -0
  106. package/dist/__tests__/trade-execution.test.js +129 -0
  107. package/dist/__tests__/trade-validator.test.d.ts +1 -0
  108. package/dist/__tests__/trade-validator.test.js +655 -0
  109. package/dist/__tests__/utils.test.d.ts +1 -0
  110. package/dist/__tests__/utils.test.js +76 -0
  111. package/dist/api/public/hyperliquid.d.ts +18 -0
  112. package/dist/api/public/hyperliquid.js +82 -0
  113. package/dist/api/public/index.d.ts +8 -0
  114. package/dist/api/public/index.js +8 -0
  115. package/dist/api/public/lighter.d.ts +24 -0
  116. package/dist/api/public/lighter.js +100 -0
  117. package/dist/api/public/pacifica.d.ts +17 -0
  118. package/dist/api/public/pacifica.js +54 -0
  119. package/dist/api/public/urls.d.ts +12 -0
  120. package/dist/api/public/urls.js +33 -0
  121. package/dist/arb/history-stats.d.ts +44 -0
  122. package/dist/arb/history-stats.js +135 -0
  123. package/dist/arb/index.d.ts +4 -0
  124. package/dist/arb/index.js +4 -0
  125. package/dist/arb/sizing.d.ts +23 -0
  126. package/dist/arb/sizing.js +96 -0
  127. package/dist/arb/state.d.ts +51 -0
  128. package/dist/arb/state.js +112 -0
  129. package/dist/arb/utils.d.ts +81 -0
  130. package/dist/arb/utils.js +267 -0
  131. package/dist/arb-history-stats.d.ts +5 -0
  132. package/dist/arb-history-stats.js +5 -0
  133. package/dist/arb-sizing.d.ts +5 -0
  134. package/dist/arb-sizing.js +5 -0
  135. package/dist/arb-state.d.ts +5 -0
  136. package/dist/arb-state.js +5 -0
  137. package/dist/arb-utils.d.ts +5 -0
  138. package/dist/arb-utils.js +5 -0
  139. package/dist/bot/conditions.d.ts +32 -0
  140. package/dist/bot/conditions.js +141 -0
  141. package/dist/bot/config.d.ts +76 -0
  142. package/dist/bot/config.js +160 -0
  143. package/dist/bot/engine.d.ts +8 -0
  144. package/dist/bot/engine.js +519 -0
  145. package/dist/bot/presets.d.ts +11 -0
  146. package/dist/bot/presets.js +296 -0
  147. package/dist/bridge-engine.d.ts +133 -0
  148. package/dist/bridge-engine.js +1487 -0
  149. package/dist/cache.d.ts +25 -0
  150. package/dist/cache.js +99 -0
  151. package/dist/cli-spec.d.ts +50 -0
  152. package/dist/cli-spec.js +75 -0
  153. package/dist/client-id-tracker.d.ts +25 -0
  154. package/dist/client-id-tracker.js +76 -0
  155. package/dist/commands/account.d.ts +3 -0
  156. package/dist/commands/account.js +425 -0
  157. package/dist/commands/agent.d.ts +3 -0
  158. package/dist/commands/agent.js +386 -0
  159. package/dist/commands/alert.d.ts +2 -0
  160. package/dist/commands/alert.js +421 -0
  161. package/dist/commands/analytics.d.ts +3 -0
  162. package/dist/commands/analytics.js +311 -0
  163. package/dist/commands/arb/index.d.ts +3 -0
  164. package/dist/commands/arb/index.js +921 -0
  165. package/dist/commands/arb-auto.d.ts +54 -0
  166. package/dist/commands/arb-auto.js +1328 -0
  167. package/dist/commands/arb-manage.d.ts +5 -0
  168. package/dist/commands/arb-manage.js +5 -0
  169. package/dist/commands/arb.d.ts +2 -0
  170. package/dist/commands/arb.js +347 -0
  171. package/dist/commands/backtest.d.ts +2 -0
  172. package/dist/commands/backtest.js +327 -0
  173. package/dist/commands/bot.d.ts +3 -0
  174. package/dist/commands/bot.js +412 -0
  175. package/dist/commands/bridge.d.ts +2 -0
  176. package/dist/commands/bridge.js +396 -0
  177. package/dist/commands/dashboard.d.ts +3 -0
  178. package/dist/commands/dashboard.js +176 -0
  179. package/dist/commands/deposit.d.ts +4 -0
  180. package/dist/commands/deposit.js +573 -0
  181. package/dist/commands/dex.d.ts +3 -0
  182. package/dist/commands/dex.js +114 -0
  183. package/dist/commands/env.d.ts +2 -0
  184. package/dist/commands/env.js +136 -0
  185. package/dist/commands/funding.d.ts +2 -0
  186. package/dist/commands/funding.js +347 -0
  187. package/dist/commands/gap.d.ts +2 -0
  188. package/dist/commands/gap.js +305 -0
  189. package/dist/commands/health.d.ts +2 -0
  190. package/dist/commands/health.js +67 -0
  191. package/dist/commands/history.d.ts +2 -0
  192. package/dist/commands/history.js +235 -0
  193. package/dist/commands/init.d.ts +15 -0
  194. package/dist/commands/init.js +266 -0
  195. package/dist/commands/jobs.d.ts +2 -0
  196. package/dist/commands/jobs.js +133 -0
  197. package/dist/commands/manage.d.ts +4 -0
  198. package/dist/commands/manage.js +309 -0
  199. package/dist/commands/market.d.ts +3 -0
  200. package/dist/commands/market.js +225 -0
  201. package/dist/commands/plan.d.ts +3 -0
  202. package/dist/commands/plan.js +95 -0
  203. package/dist/commands/portfolio.d.ts +3 -0
  204. package/dist/commands/portfolio.js +169 -0
  205. package/dist/commands/rebalance.d.ts +3 -0
  206. package/dist/commands/rebalance.js +293 -0
  207. package/dist/commands/risk.d.ts +3 -0
  208. package/dist/commands/risk.js +169 -0
  209. package/dist/commands/run.d.ts +3 -0
  210. package/dist/commands/run.js +202 -0
  211. package/dist/commands/settings.d.ts +2 -0
  212. package/dist/commands/settings.js +102 -0
  213. package/dist/commands/stream.d.ts +5 -0
  214. package/dist/commands/stream.js +123 -0
  215. package/dist/commands/trade.d.ts +3 -0
  216. package/dist/commands/trade.js +1273 -0
  217. package/dist/commands/wallet.d.ts +14 -0
  218. package/dist/commands/wallet.js +602 -0
  219. package/dist/commands/withdraw.d.ts +3 -0
  220. package/dist/commands/withdraw.js +187 -0
  221. package/dist/config.d.ts +5 -0
  222. package/dist/config.js +68 -0
  223. package/dist/cross-chain-margin.d.ts +46 -0
  224. package/dist/cross-chain-margin.js +107 -0
  225. package/dist/dashboard/server.d.ts +80 -0
  226. package/dist/dashboard/server.js +340 -0
  227. package/dist/dashboard/ui.d.ts +4 -0
  228. package/dist/dashboard/ui.js +538 -0
  229. package/dist/dashboard/ws-feeds.d.ts +29 -0
  230. package/dist/dashboard/ws-feeds.js +660 -0
  231. package/dist/dex-asset-map.d.ts +80 -0
  232. package/dist/dex-asset-map.js +201 -0
  233. package/dist/errors.d.ts +109 -0
  234. package/dist/errors.js +84 -0
  235. package/dist/event-stream.d.ts +25 -0
  236. package/dist/event-stream.js +168 -0
  237. package/dist/exchanges/hyperliquid.d.ts +212 -0
  238. package/dist/exchanges/hyperliquid.js +931 -0
  239. package/dist/exchanges/interface.d.ts +95 -0
  240. package/dist/exchanges/interface.js +5 -0
  241. package/dist/exchanges/lighter.d.ts +159 -0
  242. package/dist/exchanges/lighter.js +793 -0
  243. package/dist/exchanges/pacifica.d.ts +51 -0
  244. package/dist/exchanges/pacifica.js +248 -0
  245. package/dist/execution-log.d.ts +36 -0
  246. package/dist/execution-log.js +102 -0
  247. package/dist/funding/history.d.ts +63 -0
  248. package/dist/funding/history.js +266 -0
  249. package/dist/funding/index.d.ts +3 -0
  250. package/dist/funding/index.js +3 -0
  251. package/dist/funding/normalize.d.ts +39 -0
  252. package/dist/funding/normalize.js +66 -0
  253. package/dist/funding/rates.d.ts +45 -0
  254. package/dist/funding/rates.js +172 -0
  255. package/dist/funding-history.d.ts +5 -0
  256. package/dist/funding-history.js +5 -0
  257. package/dist/funding-rates.d.ts +5 -0
  258. package/dist/funding-rates.js +5 -0
  259. package/dist/funding.d.ts +5 -0
  260. package/dist/funding.js +5 -0
  261. package/dist/index.d.ts +2 -0
  262. package/dist/index.js +458 -0
  263. package/dist/jobs.d.ts +37 -0
  264. package/dist/jobs.js +152 -0
  265. package/dist/liquidity.d.ts +34 -0
  266. package/dist/liquidity.js +100 -0
  267. package/dist/mcp-server.d.ts +9 -0
  268. package/dist/mcp-server.js +1206 -0
  269. package/dist/pacifica/client.d.ts +111 -0
  270. package/dist/pacifica/client.js +310 -0
  271. package/dist/pacifica/constants.d.ts +27 -0
  272. package/dist/pacifica/constants.js +47 -0
  273. package/dist/pacifica/deposit.d.ts +14 -0
  274. package/dist/pacifica/deposit.js +78 -0
  275. package/dist/pacifica/index.d.ts +6 -0
  276. package/dist/pacifica/index.js +11 -0
  277. package/dist/pacifica/signing.d.ts +49 -0
  278. package/dist/pacifica/signing.js +97 -0
  279. package/dist/pacifica/types/account.d.ts +42 -0
  280. package/dist/pacifica/types/account.js +1 -0
  281. package/dist/pacifica/types/index.d.ts +6 -0
  282. package/dist/pacifica/types/index.js +6 -0
  283. package/dist/pacifica/types/lake.d.ts +18 -0
  284. package/dist/pacifica/types/lake.js +1 -0
  285. package/dist/pacifica/types/market.d.ts +64 -0
  286. package/dist/pacifica/types/market.js +1 -0
  287. package/dist/pacifica/types/order.d.ts +92 -0
  288. package/dist/pacifica/types/order.js +1 -0
  289. package/dist/pacifica/types/position.d.ts +25 -0
  290. package/dist/pacifica/types/position.js +1 -0
  291. package/dist/pacifica/types/ws.d.ts +34 -0
  292. package/dist/pacifica/types/ws.js +41 -0
  293. package/dist/pacifica/ws-client.d.ts +42 -0
  294. package/dist/pacifica/ws-client.js +180 -0
  295. package/dist/plan-executor.d.ts +48 -0
  296. package/dist/plan-executor.js +280 -0
  297. package/dist/position-history.d.ts +68 -0
  298. package/dist/position-history.js +222 -0
  299. package/dist/rebalance.d.ts +64 -0
  300. package/dist/rebalance.js +142 -0
  301. package/dist/retry.d.ts +74 -0
  302. package/dist/retry.js +129 -0
  303. package/dist/risk.d.ts +48 -0
  304. package/dist/risk.js +156 -0
  305. package/dist/settings.d.ts +19 -0
  306. package/dist/settings.js +45 -0
  307. package/dist/shared-api.d.ts +5 -0
  308. package/dist/shared-api.js +5 -0
  309. package/dist/strategies/dca.d.ts +25 -0
  310. package/dist/strategies/dca.js +114 -0
  311. package/dist/strategies/funding-arb.d.ts +15 -0
  312. package/dist/strategies/funding-arb.js +281 -0
  313. package/dist/strategies/grid.d.ts +34 -0
  314. package/dist/strategies/grid.js +185 -0
  315. package/dist/strategies/trailing-stop.d.ts +17 -0
  316. package/dist/strategies/trailing-stop.js +121 -0
  317. package/dist/strategies/twap.d.ts +20 -0
  318. package/dist/strategies/twap.js +78 -0
  319. package/dist/trade-validator.d.ts +39 -0
  320. package/dist/trade-validator.js +154 -0
  321. package/dist/utils.d.ts +38 -0
  322. package/dist/utils.js +110 -0
  323. package/package.json +63 -0
  324. package/skills/perp-cli/SKILL.md +149 -0
  325. package/skills/perp-cli/references/commands.md +143 -0
@@ -0,0 +1,110 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { classifyError, PerpError, ERROR_CODES } from "../errors.js";
3
+ describe("classifyError — pattern matching", () => {
4
+ it("classifies insufficient balance errors", () => {
5
+ const r = classifyError(new Error("Insufficient balance for order"));
6
+ expect(r.code).toBe("INSUFFICIENT_BALANCE");
7
+ expect(r.status).toBe(400);
8
+ expect(r.retryable).toBe(false);
9
+ });
10
+ it("classifies margin-specific insufficient errors before generic balance", () => {
11
+ const r = classifyError(new Error("Margin insufficient for cross order"));
12
+ expect(r.code).toBe("MARGIN_INSUFFICIENT");
13
+ expect(r.status).toBe(400);
14
+ });
15
+ it("classifies rate limit errors", () => {
16
+ const r = classifyError(new Error("429 Too Many Requests"));
17
+ expect(r.code).toBe("RATE_LIMITED");
18
+ expect(r.retryable).toBe(true);
19
+ expect(r.retryAfterMs).toBe(1000);
20
+ });
21
+ it("classifies network errors", () => {
22
+ expect(classifyError(new Error("fetch failed")).code).toBe("EXCHANGE_UNREACHABLE");
23
+ expect(classifyError(new Error("ECONNREFUSED")).code).toBe("EXCHANGE_UNREACHABLE");
24
+ expect(classifyError(new Error("ENOTFOUND api.example.com")).code).toBe("EXCHANGE_UNREACHABLE");
25
+ });
26
+ it("classifies timeout errors", () => {
27
+ expect(classifyError(new Error("Request timed out")).code).toBe("TIMEOUT");
28
+ expect(classifyError(new Error("ETIMEDOUT")).code).toBe("TIMEOUT");
29
+ expect(classifyError("timeout").code).toBe("TIMEOUT");
30
+ });
31
+ it("classifies symbol not found", () => {
32
+ const r = classifyError(new Error("Symbol XYZABC not found"));
33
+ expect(r.code).toBe("SYMBOL_NOT_FOUND");
34
+ });
35
+ it("classifies order not found", () => {
36
+ const r = classifyError(new Error("Order #12345 not found"));
37
+ expect(r.code).toBe("ORDER_NOT_FOUND");
38
+ });
39
+ it("classifies position not found", () => {
40
+ const r = classifyError(new Error("Position not found for BTC"));
41
+ expect(r.code).toBe("POSITION_NOT_FOUND");
42
+ });
43
+ it("classifies size too small", () => {
44
+ expect(classifyError(new Error("Order size too small")).code).toBe("SIZE_TOO_SMALL");
45
+ expect(classifyError(new Error("Below minimum order size")).code).toBe("SIZE_TOO_SMALL");
46
+ });
47
+ it("classifies size too large", () => {
48
+ expect(classifyError(new Error("Order size too large")).code).toBe("SIZE_TOO_LARGE");
49
+ expect(classifyError(new Error("Exceeds max position size")).code).toBe("SIZE_TOO_LARGE");
50
+ });
51
+ it("classifies signature errors", () => {
52
+ expect(classifyError(new Error("Signature verification failed")).code).toBe("SIGNATURE_FAILED");
53
+ });
54
+ it("classifies duplicate order", () => {
55
+ expect(classifyError(new Error("Duplicate order ID")).code).toBe("DUPLICATE_ORDER");
56
+ expect(classifyError(new Error("Order already exists")).code).toBe("DUPLICATE_ORDER");
57
+ });
58
+ it("classifies risk violation", () => {
59
+ expect(classifyError(new Error("Risk limit violation")).code).toBe("RISK_VIOLATION");
60
+ });
61
+ it("returns EXCHANGE_ERROR when exchange is known but message is unrecognized", () => {
62
+ const r = classifyError(new Error("Something weird happened"), "hyperliquid");
63
+ expect(r.code).toBe("EXCHANGE_ERROR");
64
+ expect(r.exchange).toBe("hyperliquid");
65
+ expect(r.retryable).toBe(true);
66
+ });
67
+ it("returns UNKNOWN when no exchange and message is unrecognized", () => {
68
+ const r = classifyError(new Error("Something weird happened"));
69
+ expect(r.code).toBe("UNKNOWN");
70
+ expect(r.retryable).toBe(false);
71
+ });
72
+ it("handles non-Error inputs", () => {
73
+ const r = classifyError("rate limit exceeded");
74
+ expect(r.code).toBe("RATE_LIMITED");
75
+ expect(r.message).toBe("rate limit exceeded");
76
+ });
77
+ });
78
+ describe("PerpError class", () => {
79
+ it("creates error with structured fields", () => {
80
+ const err = new PerpError("INSUFFICIENT_BALANCE", "Not enough USDC", { required: 100, available: 50 });
81
+ expect(err).toBeInstanceOf(Error);
82
+ expect(err.name).toBe("PerpError");
83
+ expect(err.message).toBe("Not enough USDC");
84
+ expect(err.structured.code).toBe("INSUFFICIENT_BALANCE");
85
+ expect(err.structured.status).toBe(400);
86
+ expect(err.structured.retryable).toBe(false);
87
+ expect(err.structured.details?.required).toBe(100);
88
+ });
89
+ it("has correct prototype chain", () => {
90
+ const err = new PerpError("TIMEOUT", "Request timed out");
91
+ expect(err instanceof PerpError).toBe(true);
92
+ expect(err instanceof Error).toBe(true);
93
+ });
94
+ });
95
+ describe("ERROR_CODES coverage", () => {
96
+ it("all codes have required fields", () => {
97
+ for (const [key, val] of Object.entries(ERROR_CODES)) {
98
+ expect(val.code).toBe(key);
99
+ expect(typeof val.status).toBe("number");
100
+ expect(typeof val.retryable).toBe("boolean");
101
+ }
102
+ });
103
+ it("retryable codes are all 5xx or 429", () => {
104
+ for (const val of Object.values(ERROR_CODES)) {
105
+ if (val.retryable) {
106
+ expect(val.status).toBeGreaterThanOrEqual(429);
107
+ }
108
+ }
109
+ });
110
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,276 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { startEventStream } from "../event-stream.js";
3
+ function mockAdapter(overrides) {
4
+ return {
5
+ name: "test",
6
+ getPositions: vi.fn().mockResolvedValue([]),
7
+ getOpenOrders: vi.fn().mockResolvedValue([]),
8
+ getBalance: vi.fn().mockResolvedValue({ equity: "1000", available: "800", marginUsed: "200", unrealizedPnl: "0" }),
9
+ ...overrides,
10
+ };
11
+ }
12
+ describe("startEventStream — basic polling", () => {
13
+ it("emits heartbeat after 12 cycles", async () => {
14
+ const events = [];
15
+ const controller = new AbortController();
16
+ let pollCount = 0;
17
+ const adapter = mockAdapter({
18
+ getPositions: vi.fn().mockImplementation(async () => {
19
+ pollCount++;
20
+ if (pollCount >= 13)
21
+ controller.abort();
22
+ return [];
23
+ }),
24
+ });
25
+ await startEventStream(adapter, {
26
+ intervalMs: 1,
27
+ onEvent: (e) => events.push(e),
28
+ signal: controller.signal,
29
+ });
30
+ const heartbeats = events.filter(e => e.type === "heartbeat");
31
+ expect(heartbeats.length).toBeGreaterThanOrEqual(1);
32
+ expect(heartbeats[0].data.cycle).toBe(12);
33
+ expect(heartbeats[0].exchange).toBe("test");
34
+ });
35
+ it("emits position_opened when new position appears", async () => {
36
+ const events = [];
37
+ const controller = new AbortController();
38
+ let callNum = 0;
39
+ const adapter = mockAdapter({
40
+ getPositions: vi.fn().mockImplementation(async () => {
41
+ callNum++;
42
+ if (callNum === 1)
43
+ return []; // first poll: no positions
44
+ controller.abort();
45
+ return [{ symbol: "BTC", side: "long", size: "0.1", entryPrice: "65000", unrealizedPnl: "50", liquidationPrice: "60000", markPrice: "65500" }];
46
+ }),
47
+ });
48
+ await startEventStream(adapter, {
49
+ intervalMs: 1,
50
+ onEvent: (e) => events.push(e),
51
+ signal: controller.signal,
52
+ });
53
+ const opened = events.filter(e => e.type === "position_opened");
54
+ expect(opened.length).toBe(1);
55
+ expect(opened[0].data.symbol).toBe("BTC");
56
+ expect(opened[0].data.side).toBe("long");
57
+ });
58
+ it("emits position_closed when position disappears", async () => {
59
+ const events = [];
60
+ const controller = new AbortController();
61
+ let callNum = 0;
62
+ const adapter = mockAdapter({
63
+ getPositions: vi.fn().mockImplementation(async () => {
64
+ callNum++;
65
+ if (callNum === 1)
66
+ return [{ symbol: "ETH", side: "short", size: "5", entryPrice: "2000", unrealizedPnl: "-10", liquidationPrice: "2200", markPrice: "2010" }];
67
+ controller.abort();
68
+ return []; // position gone
69
+ }),
70
+ });
71
+ await startEventStream(adapter, {
72
+ intervalMs: 1,
73
+ onEvent: (e) => events.push(e),
74
+ signal: controller.signal,
75
+ });
76
+ const closed = events.filter(e => e.type === "position_closed");
77
+ expect(closed.length).toBe(1);
78
+ expect(closed[0].data.symbol).toBe("ETH");
79
+ });
80
+ it("emits position_updated when size changes", async () => {
81
+ const events = [];
82
+ const controller = new AbortController();
83
+ let callNum = 0;
84
+ const adapter = mockAdapter({
85
+ getPositions: vi.fn().mockImplementation(async () => {
86
+ callNum++;
87
+ if (callNum === 1)
88
+ return [{ symbol: "SOL", side: "long", size: "10", entryPrice: "100", unrealizedPnl: "0", liquidationPrice: "80", markPrice: "100" }];
89
+ controller.abort();
90
+ return [{ symbol: "SOL", side: "long", size: "20", entryPrice: "100", unrealizedPnl: "0", liquidationPrice: "80", markPrice: "100" }];
91
+ }),
92
+ });
93
+ await startEventStream(adapter, {
94
+ intervalMs: 1,
95
+ onEvent: (e) => events.push(e),
96
+ signal: controller.signal,
97
+ });
98
+ const updated = events.filter(e => e.type === "position_updated");
99
+ expect(updated.length).toBe(1);
100
+ expect(updated[0].data.prevSize).toBe("10");
101
+ expect(updated[0].data.size).toBe("20");
102
+ });
103
+ });
104
+ describe("startEventStream — order diffs", () => {
105
+ it("emits order_placed for new orders", async () => {
106
+ const events = [];
107
+ const controller = new AbortController();
108
+ let callNum = 0;
109
+ const adapter = mockAdapter({
110
+ getOpenOrders: vi.fn().mockImplementation(async () => {
111
+ callNum++;
112
+ if (callNum === 1)
113
+ return [];
114
+ controller.abort();
115
+ return [{ orderId: "o1", symbol: "BTC", side: "buy", price: "60000", size: "0.1", status: "open" }];
116
+ }),
117
+ });
118
+ await startEventStream(adapter, {
119
+ intervalMs: 1,
120
+ onEvent: (e) => events.push(e),
121
+ signal: controller.signal,
122
+ });
123
+ const placed = events.filter(e => e.type === "order_placed");
124
+ expect(placed.length).toBe(1);
125
+ expect(placed[0].data.orderId).toBe("o1");
126
+ });
127
+ it("emits order_cancelled when order disappears without position change", async () => {
128
+ const events = [];
129
+ const controller = new AbortController();
130
+ let callNum = 0;
131
+ const adapter = mockAdapter({
132
+ getOpenOrders: vi.fn().mockImplementation(async () => {
133
+ callNum++;
134
+ if (callNum === 1)
135
+ return [{ orderId: "o1", symbol: "BTC", side: "buy", price: "60000", size: "0.1", status: "open" }];
136
+ controller.abort();
137
+ return []; // order gone
138
+ }),
139
+ });
140
+ await startEventStream(adapter, {
141
+ intervalMs: 1,
142
+ onEvent: (e) => events.push(e),
143
+ signal: controller.signal,
144
+ });
145
+ const cancelled = events.filter(e => e.type === "order_cancelled");
146
+ expect(cancelled.length).toBe(1);
147
+ });
148
+ });
149
+ describe("startEventStream — balance updates", () => {
150
+ it("emits balance_update when equity changes significantly", async () => {
151
+ const events = [];
152
+ const controller = new AbortController();
153
+ let callNum = 0;
154
+ const adapter = mockAdapter({
155
+ getBalance: vi.fn().mockImplementation(async () => {
156
+ callNum++;
157
+ if (callNum === 1)
158
+ return { equity: "1000.00", available: "800.00", marginUsed: "200", unrealizedPnl: "0" };
159
+ controller.abort();
160
+ return { equity: "1050.50", available: "850.50", marginUsed: "200", unrealizedPnl: "50.50" };
161
+ }),
162
+ });
163
+ await startEventStream(adapter, {
164
+ intervalMs: 1,
165
+ onEvent: (e) => events.push(e),
166
+ signal: controller.signal,
167
+ });
168
+ const balanceUpdates = events.filter(e => e.type === "balance_update");
169
+ expect(balanceUpdates.length).toBe(1);
170
+ expect(balanceUpdates[0].data.equity).toBe("1050.50");
171
+ expect(balanceUpdates[0].data.prevEquity).toBe("1000.00");
172
+ });
173
+ it("does not emit balance_update for tiny changes", async () => {
174
+ const events = [];
175
+ const controller = new AbortController();
176
+ let callNum = 0;
177
+ const adapter = mockAdapter({
178
+ getBalance: vi.fn().mockImplementation(async () => {
179
+ callNum++;
180
+ if (callNum === 1)
181
+ return { equity: "1000.00", available: "800.00", marginUsed: "200", unrealizedPnl: "0" };
182
+ controller.abort();
183
+ return { equity: "1000.005", available: "800.005", marginUsed: "200", unrealizedPnl: "0.005" };
184
+ }),
185
+ });
186
+ await startEventStream(adapter, {
187
+ intervalMs: 1,
188
+ onEvent: (e) => events.push(e),
189
+ signal: controller.signal,
190
+ });
191
+ const balanceUpdates = events.filter(e => e.type === "balance_update");
192
+ expect(balanceUpdates.length).toBe(0);
193
+ });
194
+ });
195
+ describe("startEventStream — liquidation warnings", () => {
196
+ it("emits liquidation_warning when within threshold", async () => {
197
+ const events = [];
198
+ const controller = new AbortController();
199
+ const adapter = mockAdapter({
200
+ getPositions: vi.fn().mockImplementation(async () => {
201
+ controller.abort();
202
+ // markPrice=100, liqPrice=92 → 8% distance
203
+ return [{ symbol: "SOL", side: "long", size: "10", entryPrice: "95", unrealizedPnl: "50", liquidationPrice: "92", markPrice: "100" }];
204
+ }),
205
+ });
206
+ await startEventStream(adapter, {
207
+ intervalMs: 1,
208
+ liquidationWarningPct: 10,
209
+ onEvent: (e) => events.push(e),
210
+ signal: controller.signal,
211
+ });
212
+ const warnings = events.filter(e => e.type === "liquidation_warning");
213
+ expect(warnings.length).toBe(1);
214
+ expect(warnings[0].riskLevel).toBe("warning");
215
+ expect(warnings[0].data.distancePct).toBe(8);
216
+ });
217
+ it("emits margin_call when critically close (<3%)", async () => {
218
+ const events = [];
219
+ const controller = new AbortController();
220
+ const adapter = mockAdapter({
221
+ getPositions: vi.fn().mockImplementation(async () => {
222
+ controller.abort();
223
+ // markPrice=100, liqPrice=98 → 2% distance
224
+ return [{ symbol: "SOL", side: "long", size: "10", entryPrice: "95", unrealizedPnl: "50", liquidationPrice: "98", markPrice: "100" }];
225
+ }),
226
+ });
227
+ await startEventStream(adapter, {
228
+ intervalMs: 1,
229
+ onEvent: (e) => events.push(e),
230
+ signal: controller.signal,
231
+ });
232
+ const calls = events.filter(e => e.type === "margin_call");
233
+ expect(calls.length).toBe(1);
234
+ expect(calls[0].riskLevel).toBe("critical");
235
+ });
236
+ it("skips liquidation check for N/A liquidation price", async () => {
237
+ const events = [];
238
+ const controller = new AbortController();
239
+ const adapter = mockAdapter({
240
+ getPositions: vi.fn().mockImplementation(async () => {
241
+ controller.abort();
242
+ return [{ symbol: "SOL", side: "long", size: "10", entryPrice: "95", unrealizedPnl: "50", liquidationPrice: "N/A", markPrice: "100" }];
243
+ }),
244
+ });
245
+ await startEventStream(adapter, {
246
+ intervalMs: 1,
247
+ onEvent: (e) => events.push(e),
248
+ signal: controller.signal,
249
+ });
250
+ const warnings = events.filter(e => e.type === "liquidation_warning" || e.type === "margin_call");
251
+ expect(warnings.length).toBe(0);
252
+ });
253
+ });
254
+ describe("startEventStream — error handling", () => {
255
+ it("emits error event when adapter throws", async () => {
256
+ const events = [];
257
+ const controller = new AbortController();
258
+ let callNum = 0;
259
+ const adapter = mockAdapter({
260
+ getPositions: vi.fn().mockImplementation(async () => {
261
+ callNum++;
262
+ if (callNum >= 2)
263
+ controller.abort();
264
+ throw new Error("API unreachable");
265
+ }),
266
+ });
267
+ await startEventStream(adapter, {
268
+ intervalMs: 1,
269
+ onEvent: (e) => events.push(e),
270
+ signal: controller.signal,
271
+ });
272
+ const errors = events.filter(e => e.type === "error");
273
+ expect(errors.length).toBeGreaterThanOrEqual(1);
274
+ expect(errors[0].data.message).toContain("API unreachable");
275
+ });
276
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,132 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { MockAdapter, createMockMarkets, createMockPositions, createMockOrders } from "./mock-adapter.js";
3
+ describe("ExchangeAdapter interface compliance", () => {
4
+ let adapter;
5
+ beforeEach(() => {
6
+ adapter = new MockAdapter("test-exchange");
7
+ });
8
+ it("has a name property", () => {
9
+ expect(adapter.name).toBe("test-exchange");
10
+ });
11
+ describe("getMarkets", () => {
12
+ it("returns market info array", async () => {
13
+ adapter.marketsResponse = createMockMarkets(3);
14
+ const markets = await adapter.getMarkets();
15
+ expect(markets).toHaveLength(3);
16
+ expect(markets[0]).toHaveProperty("symbol");
17
+ expect(markets[0]).toHaveProperty("markPrice");
18
+ expect(markets[0]).toHaveProperty("fundingRate");
19
+ expect(markets[0]).toHaveProperty("maxLeverage");
20
+ });
21
+ it("market info has correct types", async () => {
22
+ adapter.marketsResponse = createMockMarkets(1);
23
+ const [market] = await adapter.getMarkets();
24
+ expect(typeof market.symbol).toBe("string");
25
+ expect(typeof market.markPrice).toBe("string");
26
+ expect(typeof market.indexPrice).toBe("string");
27
+ expect(typeof market.fundingRate).toBe("string");
28
+ expect(typeof market.volume24h).toBe("string");
29
+ expect(typeof market.openInterest).toBe("string");
30
+ expect(typeof market.maxLeverage).toBe("number");
31
+ });
32
+ });
33
+ describe("getOrderbook", () => {
34
+ it("returns bids and asks", async () => {
35
+ adapter.orderbookResponse = {
36
+ bids: [["99000", "1.5"], ["98900", "2.0"]],
37
+ asks: [["100100", "0.8"], ["100200", "1.2"]],
38
+ };
39
+ const book = await adapter.getOrderbook("BTC");
40
+ expect(book.bids).toHaveLength(2);
41
+ expect(book.asks).toHaveLength(2);
42
+ expect(book.bids[0]).toEqual(["99000", "1.5"]);
43
+ expect(adapter.getCallsFor("getOrderbook")[0].args).toEqual(["BTC"]);
44
+ });
45
+ });
46
+ describe("getBalance", () => {
47
+ it("returns balance info", async () => {
48
+ const balance = await adapter.getBalance();
49
+ expect(balance).toHaveProperty("equity");
50
+ expect(balance).toHaveProperty("available");
51
+ expect(balance).toHaveProperty("marginUsed");
52
+ expect(balance).toHaveProperty("unrealizedPnl");
53
+ expect(Number(balance.equity)).toBeGreaterThan(0);
54
+ });
55
+ });
56
+ describe("getPositions", () => {
57
+ it("returns positions array", async () => {
58
+ adapter.positionsResponse = createMockPositions(2);
59
+ const positions = await adapter.getPositions();
60
+ expect(positions).toHaveLength(2);
61
+ expect(positions[0].side).toBe("long");
62
+ expect(positions[1].side).toBe("short");
63
+ });
64
+ it("position has required fields", async () => {
65
+ adapter.positionsResponse = createMockPositions(1);
66
+ const [pos] = await adapter.getPositions();
67
+ expect(pos).toHaveProperty("symbol");
68
+ expect(pos).toHaveProperty("side");
69
+ expect(pos).toHaveProperty("size");
70
+ expect(pos).toHaveProperty("entryPrice");
71
+ expect(pos).toHaveProperty("markPrice");
72
+ expect(pos).toHaveProperty("liquidationPrice");
73
+ expect(pos).toHaveProperty("unrealizedPnl");
74
+ expect(pos).toHaveProperty("leverage");
75
+ });
76
+ });
77
+ describe("getOpenOrders", () => {
78
+ it("returns orders array", async () => {
79
+ adapter.ordersResponse = createMockOrders(2);
80
+ const orders = await adapter.getOpenOrders();
81
+ expect(orders).toHaveLength(2);
82
+ expect(orders[0].side).toBe("buy");
83
+ expect(orders[1].side).toBe("sell");
84
+ });
85
+ });
86
+ describe("marketOrder", () => {
87
+ it("places market order with correct params", async () => {
88
+ await adapter.marketOrder("BTC", "buy", "0.1");
89
+ const calls = adapter.getCallsFor("marketOrder");
90
+ expect(calls).toHaveLength(1);
91
+ expect(calls[0].args).toEqual(["BTC", "buy", "0.1"]);
92
+ });
93
+ it("records sell orders", async () => {
94
+ await adapter.marketOrder("ETH", "sell", "1.0");
95
+ const calls = adapter.getCallsFor("marketOrder");
96
+ expect(calls[0].args).toEqual(["ETH", "sell", "1.0"]);
97
+ });
98
+ });
99
+ describe("limitOrder", () => {
100
+ it("places limit order with correct params", async () => {
101
+ await adapter.limitOrder("BTC", "buy", "95000", "0.05");
102
+ const calls = adapter.getCallsFor("limitOrder");
103
+ expect(calls).toHaveLength(1);
104
+ expect(calls[0].args).toEqual(["BTC", "buy", "95000", "0.05"]);
105
+ });
106
+ });
107
+ describe("cancelOrder", () => {
108
+ it("cancels with symbol and orderId", async () => {
109
+ await adapter.cancelOrder("BTC", "12345");
110
+ const calls = adapter.getCallsFor("cancelOrder");
111
+ expect(calls[0].args).toEqual(["BTC", "12345"]);
112
+ });
113
+ });
114
+ describe("cancelAllOrders", () => {
115
+ it("cancels all for specific symbol", async () => {
116
+ await adapter.cancelAllOrders("BTC");
117
+ const calls = adapter.getCallsFor("cancelAllOrders");
118
+ expect(calls[0].args).toEqual(["BTC"]);
119
+ });
120
+ it("cancels all without symbol", async () => {
121
+ await adapter.cancelAllOrders();
122
+ const calls = adapter.getCallsFor("cancelAllOrders");
123
+ expect(calls[0].args).toEqual([undefined]);
124
+ });
125
+ });
126
+ describe("adapter type safety", () => {
127
+ it("satisfies ExchangeAdapter interface", () => {
128
+ const _check = adapter;
129
+ expect(_check).toBeDefined();
130
+ });
131
+ });
132
+ });
@@ -0,0 +1,69 @@
1
+ import type { ExchangeAdapter, ExchangeMarketInfo, ExchangePosition, ExchangeOrder, ExchangeBalance, ExchangeTrade, ExchangeFundingPayment, ExchangeKline } from "../../exchanges/interface.js";
2
+ /**
3
+ * Mock adapter for unit testing. Records all calls and returns configurable responses.
4
+ */
5
+ export declare class MockAdapter implements ExchangeAdapter {
6
+ readonly name: string;
7
+ calls: {
8
+ method: string;
9
+ args: unknown[];
10
+ }[];
11
+ marketsResponse: ExchangeMarketInfo[];
12
+ orderbookResponse: {
13
+ bids: [string, string][];
14
+ asks: [string, string][];
15
+ };
16
+ balanceResponse: ExchangeBalance;
17
+ positionsResponse: ExchangePosition[];
18
+ ordersResponse: ExchangeOrder[];
19
+ orderResult: unknown;
20
+ constructor(name?: string);
21
+ private record;
22
+ getCallsFor(method: string): {
23
+ method: string;
24
+ args: unknown[];
25
+ }[];
26
+ reset(): void;
27
+ getMarkets(): Promise<ExchangeMarketInfo[]>;
28
+ getOrderbook(symbol: string): Promise<{
29
+ bids: [string, string][];
30
+ asks: [string, string][];
31
+ }>;
32
+ getBalance(): Promise<ExchangeBalance>;
33
+ getPositions(): Promise<ExchangePosition[]>;
34
+ getOpenOrders(): Promise<ExchangeOrder[]>;
35
+ marketOrder(symbol: string, side: "buy" | "sell", size: string): Promise<unknown>;
36
+ limitOrder(symbol: string, side: "buy" | "sell", price: string, size: string): Promise<unknown>;
37
+ editOrder(symbol: string, orderId: string, price: string, size: string): Promise<unknown>;
38
+ cancelOrder(symbol: string, orderId: string): Promise<{
39
+ status: string;
40
+ }>;
41
+ cancelAllOrders(symbol?: string): Promise<{
42
+ status: string;
43
+ }>;
44
+ setLeverage(symbol: string, leverage: number, marginMode?: "cross" | "isolated"): Promise<{
45
+ symbol: string;
46
+ leverage: number;
47
+ marginMode: "cross" | "isolated" | undefined;
48
+ }>;
49
+ stopOrder(symbol: string, side: "buy" | "sell", size: string, triggerPrice: string, opts?: {
50
+ limitPrice?: string;
51
+ reduceOnly?: boolean;
52
+ }): Promise<unknown>;
53
+ getRecentTrades(symbol: string, limit?: number): Promise<ExchangeTrade[]>;
54
+ getFundingHistory(symbol: string, limit?: number): Promise<{
55
+ time: number;
56
+ rate: string;
57
+ price: string;
58
+ }[]>;
59
+ getKlines(symbol: string, interval: string, startTime: number, endTime: number): Promise<ExchangeKline[]>;
60
+ getOrderHistory(limit?: number): Promise<ExchangeOrder[]>;
61
+ getTradeHistory(limit?: number): Promise<ExchangeTrade[]>;
62
+ getFundingPayments(limit?: number): Promise<ExchangeFundingPayment[]>;
63
+ }
64
+ /** Factory to create mock markets data */
65
+ export declare function createMockMarkets(count?: number): ExchangeMarketInfo[];
66
+ /** Factory to create mock positions */
67
+ export declare function createMockPositions(count?: number): ExchangePosition[];
68
+ /** Factory to create mock orders */
69
+ export declare function createMockOrders(count?: number): ExchangeOrder[];