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,660 @@
1
+ /**
2
+ * WebSocket Feed Manager for Dashboard.
3
+ *
4
+ * Connects to exchange WS APIs for real-time account data (balance, positions, orders).
5
+ * Falls back to REST polling if WS connection fails.
6
+ * Arb/market data stays on REST (cross-exchange aggregation, no single WS covers it).
7
+ */
8
+ import { WebSocket as NodeWebSocket } from "ws";
9
+ const EMPTY_BALANCE = { equity: "0", available: "0", marginUsed: "0", unrealizedPnl: "0" };
10
+ const MAX_RECONNECT_DELAY = 30_000;
11
+ // ── Base WS Feed ──
12
+ class ExchangeWsFeed {
13
+ exchange;
14
+ onUpdate;
15
+ ws = null;
16
+ state = { balance: { ...EMPTY_BALANCE }, positions: [], orders: [], lastUpdate: 0, mode: "connecting" };
17
+ reconnectDelay = 1000;
18
+ reconnectTimer = null;
19
+ closed = false;
20
+ restFallbackTimer = null;
21
+ wsDataTimer = null;
22
+ wsDataReceived = false;
23
+ /** How long to wait for WS data before falling back to REST (ms) */
24
+ WS_DATA_TIMEOUT = 5000;
25
+ constructor(exchange, onUpdate) {
26
+ this.exchange = exchange;
27
+ this.onUpdate = onUpdate;
28
+ }
29
+ getState() { return this.state; }
30
+ /** Start a timer: if no WS data arrives within WS_DATA_TIMEOUT, switch to REST */
31
+ startWsDataTimeout() {
32
+ if (this.wsDataTimer)
33
+ clearTimeout(this.wsDataTimer);
34
+ this.wsDataReceived = false;
35
+ this.wsDataTimer = setTimeout(() => {
36
+ if (!this.wsDataReceived && !this.closed) {
37
+ this.startRestFallback();
38
+ }
39
+ }, this.WS_DATA_TIMEOUT);
40
+ }
41
+ emitUpdate() {
42
+ this.state.lastUpdate = Date.now();
43
+ this.wsDataReceived = true;
44
+ // WS is delivering data — stop REST fallback if running
45
+ if (this.state.mode === "rest" && this.ws?.readyState === NodeWebSocket.OPEN) {
46
+ this.state.mode = "ws";
47
+ this.stopRestFallback();
48
+ }
49
+ this.onUpdate(this.state);
50
+ // Also write to file cache for CLI cross-process sharing
51
+ this.writeToCache().catch(() => { });
52
+ }
53
+ async writeToCache() {
54
+ try {
55
+ const { setCached, TTL_ACCOUNT } = await import("../cache.js");
56
+ const name = this.exchange.name;
57
+ setCached(`dash:${name}:balance`, this.state.balance, TTL_ACCOUNT);
58
+ setCached(`dash:${name}:positions`, this.state.positions, TTL_ACCOUNT);
59
+ setCached(`dash:${name}:orders`, this.state.orders, TTL_ACCOUNT);
60
+ }
61
+ catch { /* non-fatal */ }
62
+ }
63
+ scheduleReconnect() {
64
+ if (this.closed)
65
+ return;
66
+ this.reconnectTimer = setTimeout(async () => {
67
+ try {
68
+ await this.connect();
69
+ this.reconnectDelay = 1000; // reset on success
70
+ }
71
+ catch {
72
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY);
73
+ this.scheduleReconnect();
74
+ }
75
+ }, this.reconnectDelay);
76
+ }
77
+ startRestFallback() {
78
+ if (this.restFallbackTimer || this.closed)
79
+ return;
80
+ this.state.mode = "rest";
81
+ const doRestPoll = async () => {
82
+ try {
83
+ const adapter = this.exchange.adapter;
84
+ const [balance, positions, orders] = await Promise.all([
85
+ adapter.getBalance(),
86
+ adapter.getPositions(),
87
+ adapter.getOpenOrders(),
88
+ ]);
89
+ this.state.balance = balance;
90
+ this.state.positions = positions.filter(p => Number(p.size) !== 0);
91
+ this.state.orders = orders;
92
+ this.emitUpdate();
93
+ }
94
+ catch { /* ignore */ }
95
+ };
96
+ doRestPoll(); // immediate first fetch
97
+ this.restFallbackTimer = setInterval(doRestPoll, 5000);
98
+ }
99
+ stopRestFallback() {
100
+ if (this.restFallbackTimer) {
101
+ clearInterval(this.restFallbackTimer);
102
+ this.restFallbackTimer = null;
103
+ }
104
+ }
105
+ close() {
106
+ this.closed = true;
107
+ this.stopRestFallback();
108
+ if (this.reconnectTimer)
109
+ clearTimeout(this.reconnectTimer);
110
+ if (this.wsDataTimer)
111
+ clearTimeout(this.wsDataTimer);
112
+ if (this.ws) {
113
+ this.ws.close();
114
+ this.ws = null;
115
+ }
116
+ }
117
+ }
118
+ // ── Hyperliquid WS Feed ──
119
+ class HyperliquidFeed extends ExchangeWsFeed {
120
+ address;
121
+ dex;
122
+ balanceTimer = null;
123
+ constructor(exchange, onUpdate) {
124
+ super(exchange, onUpdate);
125
+ // Extract address and dex from adapter
126
+ const adapter = exchange.adapter;
127
+ this.address = String(adapter._address ?? adapter.address ?? "");
128
+ this.dex = String(adapter._dex ?? "");
129
+ }
130
+ get wsUrl() { return "wss://api.hyperliquid.xyz/ws"; }
131
+ async connect() {
132
+ if (!this.address) {
133
+ this.startRestFallback();
134
+ return;
135
+ }
136
+ // HIP-3 dex accounts: webData2 doesn't support dex param — use REST
137
+ if (this.dex) {
138
+ this.startRestFallback();
139
+ return;
140
+ }
141
+ return new Promise((resolve, reject) => {
142
+ try {
143
+ this.ws = new NodeWebSocket(this.wsUrl);
144
+ this.ws.on("open", () => {
145
+ this.state.mode = "ws";
146
+ // Subscribe to webData2 (positions + orders in real-time)
147
+ this.ws.send(JSON.stringify({
148
+ method: "subscribe",
149
+ subscription: { type: "webData2", user: this.address },
150
+ }));
151
+ // Balance via REST (webData2 clearinghouse doesn't include dex pool funds)
152
+ this.startBalancePolling();
153
+ // Start data timeout — switch to REST if no data within 5s
154
+ this.startWsDataTimeout();
155
+ resolve();
156
+ });
157
+ this.ws.on("message", (raw) => {
158
+ try {
159
+ const msg = JSON.parse(String(raw));
160
+ if (msg.channel === "webData2" && msg.data) {
161
+ this.handleWebData2(msg.data);
162
+ }
163
+ }
164
+ catch { /* ignore parse errors */ }
165
+ });
166
+ this.ws.on("close", () => {
167
+ this.stopBalancePolling();
168
+ if (!this.closed) {
169
+ this.startRestFallback();
170
+ this.scheduleReconnect();
171
+ }
172
+ });
173
+ this.ws.on("error", (err) => {
174
+ if (this.state.mode === "connecting") {
175
+ this.startRestFallback();
176
+ reject(err);
177
+ }
178
+ });
179
+ }
180
+ catch (err) {
181
+ this.startRestFallback();
182
+ reject(err);
183
+ }
184
+ });
185
+ }
186
+ /** Poll balance via REST (webData2 clearinghouse doesn't include dex pool funds) */
187
+ startBalancePolling() {
188
+ if (this.balanceTimer)
189
+ return;
190
+ const poll = async () => {
191
+ try {
192
+ this.state.balance = await this.exchange.adapter.getBalance();
193
+ }
194
+ catch { /* ignore */ }
195
+ };
196
+ poll(); // immediate
197
+ this.balanceTimer = setInterval(poll, 5000);
198
+ }
199
+ stopBalancePolling() {
200
+ if (this.balanceTimer) {
201
+ clearInterval(this.balanceTimer);
202
+ this.balanceTimer = null;
203
+ }
204
+ }
205
+ handleWebData2(data) {
206
+ try {
207
+ // Balance is handled by REST polling (includes dex pool funds)
208
+ // WS handles positions + orders only
209
+ const chs = data.clearinghouseState;
210
+ if (chs) {
211
+ const assetPositions = (chs.assetPositions ?? []);
212
+ this.state.positions = assetPositions
213
+ .filter((ap) => {
214
+ const pos = (ap.position ?? ap);
215
+ return Number(pos.szi ?? 0) !== 0;
216
+ })
217
+ .map((ap) => {
218
+ const pos = (ap.position ?? ap);
219
+ const szi = Number(pos.szi);
220
+ return {
221
+ symbol: String(pos.coin ?? ""),
222
+ side: (szi > 0 ? "long" : "short"),
223
+ size: String(Math.abs(szi)),
224
+ entryPrice: pos.entryPx ?? "0",
225
+ markPrice: String(pos.positionValue ? (Number(pos.positionValue) / Math.abs(szi)).toFixed(2) : "0"),
226
+ liquidationPrice: pos.liquidationPx ?? "N/A",
227
+ unrealizedPnl: pos.unrealizedPnl ?? "0",
228
+ leverage: Number(ap.leverage?.value ?? 1),
229
+ };
230
+ });
231
+ }
232
+ // Open orders
233
+ const openOrders = data.openOrders;
234
+ if (openOrders) {
235
+ this.state.orders = openOrders.map((o) => ({
236
+ orderId: String(o.oid ?? ""),
237
+ symbol: String(o.coin ?? ""),
238
+ side: o.side === "B" ? "buy" : "sell",
239
+ price: String(o.limitPx ?? "0"),
240
+ size: String(o.sz ?? ""),
241
+ filled: "0",
242
+ status: "open",
243
+ type: String(o.orderType ?? "limit"),
244
+ }));
245
+ }
246
+ this.emitUpdate();
247
+ }
248
+ catch { /* ignore */ }
249
+ }
250
+ close() {
251
+ this.stopBalancePolling();
252
+ super.close();
253
+ }
254
+ }
255
+ // ── Pacifica WS Feed ──
256
+ class PacificaFeed extends ExchangeWsFeed {
257
+ account;
258
+ markPriceTimer = null;
259
+ markPrices = new Map();
260
+ constructor(exchange, onUpdate) {
261
+ super(exchange, onUpdate);
262
+ const adapter = exchange.adapter;
263
+ this.account = String(adapter.publicKey ?? adapter.account ?? "");
264
+ }
265
+ get wsUrl() { return "wss://ws.pacifica.fi/ws"; }
266
+ async connect() {
267
+ if (!this.account) {
268
+ this.startRestFallback();
269
+ return;
270
+ }
271
+ return new Promise((resolve, reject) => {
272
+ try {
273
+ this.ws = new NodeWebSocket(this.wsUrl);
274
+ this.ws.on("open", () => {
275
+ this.state.mode = "ws";
276
+ // Subscribe to private channels
277
+ const sub = (source) => this.ws.send(JSON.stringify({ method: "subscribe", params: { source, account: this.account } }));
278
+ sub("account_info");
279
+ sub("account_positions");
280
+ sub("account_order_updates");
281
+ // Heartbeat
282
+ this._heartbeat = setInterval(() => {
283
+ if (this.ws?.readyState === NodeWebSocket.OPEN) {
284
+ this.ws.send(JSON.stringify({ method: "ping" }));
285
+ }
286
+ }, 30000);
287
+ // Poll mark prices (WS positions don't include them)
288
+ this.startMarkPricePolling();
289
+ // Start data timeout — switch to REST if no data within 5s
290
+ this.startWsDataTimeout();
291
+ resolve();
292
+ });
293
+ this.ws.on("message", (raw) => {
294
+ try {
295
+ const msg = JSON.parse(String(raw));
296
+ const ch = msg.channel || msg.source;
297
+ if (ch === "account_info")
298
+ this.handleAccountInfo(msg.data ?? msg);
299
+ else if (ch === "account_positions")
300
+ this.handlePositions(msg.data ?? msg);
301
+ else if (ch === "account_order_updates")
302
+ this.handleOrders(msg.data ?? msg);
303
+ }
304
+ catch { /* ignore */ }
305
+ });
306
+ this.ws.on("close", () => {
307
+ this.clearHeartbeat();
308
+ if (!this.closed) {
309
+ this.startRestFallback();
310
+ this.scheduleReconnect();
311
+ }
312
+ });
313
+ this.ws.on("error", (err) => {
314
+ if (this.state.mode === "connecting") {
315
+ this.startRestFallback();
316
+ reject(err);
317
+ }
318
+ });
319
+ }
320
+ catch (err) {
321
+ this.startRestFallback();
322
+ reject(err);
323
+ }
324
+ });
325
+ }
326
+ _heartbeat = null;
327
+ clearHeartbeat() {
328
+ if (this._heartbeat) {
329
+ clearInterval(this._heartbeat);
330
+ this._heartbeat = null;
331
+ }
332
+ }
333
+ /** Poll mark prices from adapter (WS positions don't include mark prices) */
334
+ startMarkPricePolling() {
335
+ if (this.markPriceTimer)
336
+ return;
337
+ const poll = async () => {
338
+ try {
339
+ // Use adapter's getPositions which includes mark prices
340
+ const positions = await this.exchange.adapter.getPositions();
341
+ for (const p of positions) {
342
+ this.markPrices.set(p.symbol, p.markPrice);
343
+ }
344
+ // Update existing WS positions with mark prices + compute PnL
345
+ if (this.state.positions.length > 0) {
346
+ let changed = false;
347
+ for (const pos of this.state.positions) {
348
+ const mark = this.markPrices.get(pos.symbol);
349
+ if (mark && mark !== "0" && pos.markPrice === "0") {
350
+ pos.markPrice = mark;
351
+ const markNum = Number(mark);
352
+ const entry = Number(pos.entryPrice);
353
+ const amount = Number(pos.size);
354
+ const pnl = pos.side === "long"
355
+ ? (markNum - entry) * amount
356
+ : (entry - markNum) * amount;
357
+ pos.unrealizedPnl = pnl.toFixed(4);
358
+ changed = true;
359
+ }
360
+ }
361
+ if (changed)
362
+ this.emitUpdate();
363
+ }
364
+ }
365
+ catch { /* ignore */ }
366
+ };
367
+ poll(); // immediate
368
+ this.markPriceTimer = setInterval(poll, 5000);
369
+ }
370
+ stopMarkPricePolling() {
371
+ if (this.markPriceTimer) {
372
+ clearInterval(this.markPriceTimer);
373
+ this.markPriceTimer = null;
374
+ }
375
+ }
376
+ handleAccountInfo(data) {
377
+ // Abbreviated fields: ae=account_equity, as=available_to_spend, mu=margin_used
378
+ const eq = String(data.ae ?? data.account_equity ?? this.state.balance.equity);
379
+ const av = String(data.as ?? data.available_to_spend ?? this.state.balance.available);
380
+ const mu = String(data.mu ?? data.margin_used ?? this.state.balance.marginUsed);
381
+ this.state.balance = { equity: eq, available: av, marginUsed: mu, unrealizedPnl: this.state.balance.unrealizedPnl };
382
+ this.emitUpdate();
383
+ }
384
+ handlePositions(data) {
385
+ if (!Array.isArray(data))
386
+ return;
387
+ // WS uses abbreviated fields: s=symbol, d=side, a=amount, p=entry_price, l=liquidation, m=margin
388
+ this.state.positions = data
389
+ .filter((p) => {
390
+ const amount = Number(p.a ?? p.amount ?? 0);
391
+ const symbol = p.s ?? p.symbol;
392
+ return amount !== 0 && symbol;
393
+ })
394
+ .map((p) => {
395
+ const symbol = String(p.s ?? p.symbol ?? "");
396
+ const side = String(p.d ?? p.side ?? "") === "bid" ? "long" : "short";
397
+ const size = Number(p.a ?? p.amount ?? 0);
398
+ const entryPrice = Number(p.p ?? p.entry_price ?? 0);
399
+ const mark = this.markPrices.get(symbol);
400
+ const markNum = mark ? Number(mark) : 0;
401
+ const pnl = markNum > 0
402
+ ? (side === "long" ? (markNum - entryPrice) * size : (entryPrice - markNum) * size)
403
+ : 0;
404
+ return {
405
+ symbol,
406
+ side,
407
+ size: String(size),
408
+ entryPrice: String(entryPrice),
409
+ markPrice: mark ?? "0",
410
+ liquidationPrice: String(p.l ?? p.liquidation_price ?? "N/A"),
411
+ unrealizedPnl: pnl.toFixed(4),
412
+ leverage: Number(p.leverage ?? 1),
413
+ };
414
+ });
415
+ this.emitUpdate();
416
+ }
417
+ handleOrders(data) {
418
+ if (!Array.isArray(data))
419
+ return;
420
+ this.state.orders = data
421
+ .filter((o) => String(o.os ?? o.order_status ?? "") === "open")
422
+ .map((o) => ({
423
+ orderId: String(o.i ?? o.order_id ?? ""),
424
+ symbol: String(o.s ?? o.symbol ?? ""),
425
+ side: (o.d ?? o.side) === "bid" ? "buy" : "sell",
426
+ price: String(o.p ?? o.price ?? "0"),
427
+ size: String(o.a ?? o.amount ?? ""),
428
+ filled: String(o.f ?? o.filled ?? "0"),
429
+ status: "open",
430
+ type: String(o.ot ?? o.order_type ?? ""),
431
+ }));
432
+ this.emitUpdate();
433
+ }
434
+ close() {
435
+ this.clearHeartbeat();
436
+ this.stopMarkPricePolling();
437
+ super.close();
438
+ }
439
+ }
440
+ // ── Lighter WS Feed ──
441
+ // WS: wss://mainnet.zklighter.elliot.ai/stream
442
+ // Auth: signer.createAuthToken() passed as `auth` field in subscribe messages
443
+ // Channels: account_all_positions/{index} (auth), account_all_orders/{index} (auth),
444
+ // user_stats/{index} (public — balance info)
445
+ class LighterFeed extends ExchangeWsFeed {
446
+ accountIndex;
447
+ constructor(exchange, onUpdate) {
448
+ super(exchange, onUpdate);
449
+ const adapter = exchange.adapter;
450
+ this.accountIndex = Number(adapter._accountIndex ?? -1);
451
+ }
452
+ get wsUrl() { return "wss://mainnet.zklighter.elliot.ai/stream"; }
453
+ async getAuthToken() {
454
+ try {
455
+ const adapter = this.exchange.adapter;
456
+ if (adapter.isReadOnly || !adapter.signer)
457
+ return null;
458
+ const deadline = Math.floor(Date.now() / 1000) + 3600;
459
+ const auth = await adapter.signer.createAuthToken(deadline);
460
+ return auth.authToken;
461
+ }
462
+ catch {
463
+ return null;
464
+ }
465
+ }
466
+ async connect() {
467
+ if (this.accountIndex < 0) {
468
+ this.startRestFallback();
469
+ return;
470
+ }
471
+ // Get auth token for private channel subscriptions
472
+ const authToken = await this.getAuthToken();
473
+ if (!authToken) {
474
+ // No auth available — use REST fallback
475
+ this.startRestFallback();
476
+ return;
477
+ }
478
+ return new Promise((resolve, reject) => {
479
+ try {
480
+ this.ws = new NodeWebSocket(this.wsUrl);
481
+ this.ws.on("open", () => {
482
+ this.state.mode = "ws";
483
+ // Subscribe to authenticated channels (positions + orders)
484
+ const authChannels = [
485
+ `account_all_positions/${this.accountIndex}`,
486
+ `account_all_orders/${this.accountIndex}`,
487
+ ];
488
+ for (const channel of authChannels) {
489
+ this.ws.send(JSON.stringify({ type: "subscribe", channel, auth: authToken }));
490
+ }
491
+ // Subscribe to public channel for balance (no auth needed)
492
+ this.ws.send(JSON.stringify({ type: "subscribe", channel: `user_stats/${this.accountIndex}` }));
493
+ // Start data timeout — switch to REST if no data within 5s
494
+ this.startWsDataTimeout();
495
+ resolve();
496
+ });
497
+ this.ws.on("message", (raw) => {
498
+ try {
499
+ const msg = JSON.parse(String(raw));
500
+ this.handleMessage(msg);
501
+ }
502
+ catch { /* ignore */ }
503
+ });
504
+ this.ws.on("close", () => {
505
+ if (!this.closed) {
506
+ this.startRestFallback();
507
+ this.scheduleReconnect();
508
+ }
509
+ });
510
+ this.ws.on("error", (err) => {
511
+ if (this.state.mode === "connecting") {
512
+ this.startRestFallback();
513
+ reject(err);
514
+ }
515
+ });
516
+ }
517
+ catch (err) {
518
+ this.startRestFallback();
519
+ reject(err);
520
+ }
521
+ });
522
+ }
523
+ /** Cached position map: market_id → position data (for incremental updates) */
524
+ positionMap = new Map();
525
+ handleMessage(msg) {
526
+ const type = String(msg.type ?? "");
527
+ // Balance: user_stats channel (public, no auth) provides portfolio_value, available_balance, etc.
528
+ if (type.includes("user_stats")) {
529
+ const stats = (msg.stats ?? msg);
530
+ // total_stats includes cross-margin + isolated totals
531
+ const src = (stats.total_stats ?? stats);
532
+ const equity = String(src.portfolio_value ?? src.collateral ?? this.state.balance.equity);
533
+ const available = String(src.available_balance ?? this.state.balance.available);
534
+ const marginUsed = String(src.margin_usage ?? this.state.balance.marginUsed);
535
+ this.state.balance = { equity, available, marginUsed, unrealizedPnl: this.state.balance.unrealizedPnl };
536
+ this.emitUpdate();
537
+ }
538
+ // Positions: subscribed/ = full snapshot, update/ = incremental
539
+ if (type.includes("account_all_positions")) {
540
+ const positions = msg.positions;
541
+ if (positions) {
542
+ const isSnapshot = type.startsWith("subscribed/");
543
+ if (isSnapshot) {
544
+ // Full snapshot — replace everything
545
+ this.positionMap.clear();
546
+ for (const [id, p] of Object.entries(positions)) {
547
+ this.positionMap.set(id, p);
548
+ }
549
+ }
550
+ else {
551
+ // Incremental update — merge only provided entries
552
+ for (const [id, p] of Object.entries(positions)) {
553
+ this.positionMap.set(id, { ...this.positionMap.get(id), ...p });
554
+ }
555
+ }
556
+ // Rebuild positions from map
557
+ this.state.positions = [...this.positionMap.values()]
558
+ .filter(p => Number(p.position ?? 0) !== 0)
559
+ .map(p => {
560
+ const posSize = Number(p.position ?? 0);
561
+ return {
562
+ symbol: String(p.symbol ?? `Market-${p.market_id}`),
563
+ side: (Number(p.sign) > 0 ? "long" : "short"),
564
+ size: String(Math.abs(posSize)),
565
+ entryPrice: String(p.avg_entry_price ?? "0"),
566
+ markPrice: posSize !== 0
567
+ ? String((Number(p.position_value ?? 0) / Math.abs(posSize)).toFixed(4))
568
+ : "0",
569
+ liquidationPrice: String(p.liquidation_price || "N/A"),
570
+ unrealizedPnl: String(p.unrealized_pnl ?? "0"),
571
+ leverage: Number(p.initial_margin_fraction ?? 0) > 0
572
+ ? Math.round(100 / Number(p.initial_margin_fraction))
573
+ : 1,
574
+ };
575
+ });
576
+ this.emitUpdate();
577
+ }
578
+ }
579
+ // Orders: subscribed/ = full snapshot, update/ = incremental
580
+ if (type.includes("account_all_orders")) {
581
+ const orders = msg.orders;
582
+ if (orders) {
583
+ const allOrders = Object.values(orders).flat();
584
+ this.state.orders = allOrders
585
+ .filter(o => String(o.status ?? "") === "open")
586
+ .map(o => ({
587
+ orderId: String(o.order_id ?? o.order_index ?? ""),
588
+ symbol: String(o.symbol ?? ""),
589
+ side: (o.side === "buy" || o.is_ask === false ? "buy" : "sell"),
590
+ price: String(o.price ?? "0"),
591
+ size: String(o.remaining_base_amount ?? o.initial_base_amount ?? ""),
592
+ filled: String(o.filled_base_amount ?? "0"),
593
+ status: "open",
594
+ type: String(o.type ?? "limit"),
595
+ }));
596
+ this.emitUpdate();
597
+ }
598
+ }
599
+ }
600
+ }
601
+ // ── WsFeedManager ──
602
+ export class WsFeedManager {
603
+ feeds = new Map();
604
+ onUpdate;
605
+ constructor(exchanges, opts) {
606
+ this.onUpdate = opts.onUpdate;
607
+ for (const ex of exchanges) {
608
+ const handler = (state) => this.onUpdate(ex.name, state);
609
+ let feed;
610
+ if (ex.name === "hyperliquid" || ex.name.startsWith("hl:")) {
611
+ feed = new HyperliquidFeed(ex, handler);
612
+ }
613
+ else if (ex.name === "pacifica") {
614
+ feed = new PacificaFeed(ex, handler);
615
+ }
616
+ else if (ex.name === "lighter") {
617
+ feed = new LighterFeed(ex, handler);
618
+ }
619
+ else {
620
+ // Unknown exchange — REST fallback only
621
+ feed = new RestOnlyFeed(ex, handler);
622
+ }
623
+ this.feeds.set(ex.name, feed);
624
+ }
625
+ if (opts.signal) {
626
+ opts.signal.addEventListener("abort", () => this.close(), { once: true });
627
+ }
628
+ }
629
+ async start() {
630
+ const results = await Promise.allSettled([...this.feeds.values()].map((f) => f.connect()));
631
+ // Log failures but don't throw — feeds fall back to REST
632
+ for (const r of results) {
633
+ if (r.status === "rejected") {
634
+ // Feed will have activated REST fallback internally
635
+ }
636
+ }
637
+ }
638
+ getState(exchange) {
639
+ return this.feeds.get(exchange)?.getState() ?? null;
640
+ }
641
+ getAllStates() {
642
+ const result = new Map();
643
+ for (const [name, feed] of this.feeds) {
644
+ result.set(name, feed.getState());
645
+ }
646
+ return result;
647
+ }
648
+ close() {
649
+ for (const feed of this.feeds.values()) {
650
+ feed.close();
651
+ }
652
+ }
653
+ }
654
+ // Fallback: REST-only feed for unknown exchanges
655
+ class RestOnlyFeed extends ExchangeWsFeed {
656
+ get wsUrl() { return ""; }
657
+ async connect() {
658
+ this.startRestFallback();
659
+ }
660
+ }