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,340 @@
1
+ /**
2
+ * Live Dashboard Server — HTTP + WebSocket for real-time portfolio monitoring.
3
+ *
4
+ * Polls all configured exchange adapters and pushes updates to connected clients.
5
+ * Includes cross-exchange arb data: funding rate comparison + dex arb scan.
6
+ */
7
+ import { createServer } from "http";
8
+ import { WebSocketServer, WebSocket } from "ws";
9
+ import { getUI } from "./ui.js";
10
+ import { WsFeedManager } from "./ws-feeds.js";
11
+ // Cached arb data (polled less frequently)
12
+ let cachedArb = { opportunities: [], dexArb: [], dexAssets: [], dexNames: [], exchangeStatus: {} };
13
+ // Cached market data (polled with arb cycle — market metadata rarely changes)
14
+ const cachedMarkets = new Map();
15
+ /**
16
+ * Find an available port starting from the given port.
17
+ */
18
+ async function findPort(startPort) {
19
+ return new Promise((resolve, reject) => {
20
+ const srv = createServer();
21
+ srv.listen(startPort, () => {
22
+ const addr = srv.address();
23
+ const port = typeof addr === "object" && addr ? addr.port : startPort;
24
+ srv.close(() => resolve(port));
25
+ });
26
+ srv.on("error", (err) => {
27
+ if (err.code === "EADDRINUSE") {
28
+ resolve(findPort(startPort + 1));
29
+ }
30
+ else {
31
+ reject(err);
32
+ }
33
+ });
34
+ });
35
+ }
36
+ /**
37
+ * Fetch arb data: cross-exchange funding rates + HIP-3 dex arb.
38
+ */
39
+ async function pollArbData() {
40
+ const opportunities = [];
41
+ const dexArb = [];
42
+ let exchangeStatus = {};
43
+ try {
44
+ const { fetchAllFundingRates } = await import("../funding/rates.js");
45
+ const snapshot = await fetchAllFundingRates({ minSpread: 5 });
46
+ exchangeStatus = snapshot.exchangeStatus;
47
+ for (const sym of snapshot.symbols.slice(0, 20)) {
48
+ opportunities.push({
49
+ symbol: sym.symbol,
50
+ longExchange: sym.longExchange,
51
+ shortExchange: sym.shortExchange,
52
+ spreadAnnual: sym.maxSpreadAnnual,
53
+ estHourlyUsd: sym.estHourlyIncomeUsd,
54
+ rates: sym.rates.map((r) => ({
55
+ exchange: r.exchange,
56
+ annualizedPct: r.annualizedPct,
57
+ hourlyRate: r.hourlyRate,
58
+ markPrice: r.markPrice,
59
+ })),
60
+ });
61
+ }
62
+ }
63
+ catch {
64
+ // funding rates unavailable
65
+ }
66
+ const dexAssets = [];
67
+ const dexNamesSet = new Set();
68
+ try {
69
+ const { fetchAllDexAssets, findDexArbPairs } = await import("../dex-asset-map.js");
70
+ const { annualizeRate } = await import("../funding/normalize.js");
71
+ // Single fetch — used for both arb pairs and rate comparison table
72
+ const allAssets = await fetchAllDexAssets();
73
+ const pairs = findDexArbPairs(allAssets, { minAnnualSpread: 10 });
74
+ for (const p of pairs.slice(0, 15)) {
75
+ dexArb.push({
76
+ underlying: p.underlying,
77
+ longDex: `${p.long.dex}:${p.long.base}`,
78
+ shortDex: `${p.short.dex}:${p.short.base}`,
79
+ annualSpread: p.annualSpread,
80
+ priceGapPct: p.priceGapPct,
81
+ viability: p.viability,
82
+ });
83
+ }
84
+ const byBase = new Map();
85
+ for (const a of allAssets) {
86
+ dexNamesSet.add(a.dex);
87
+ if (!byBase.has(a.base))
88
+ byBase.set(a.base, []);
89
+ byBase.get(a.base).push(a);
90
+ }
91
+ // Only show assets on 2+ dexes, sorted by max spread
92
+ for (const [base, assets] of byBase) {
93
+ if (assets.length < 2)
94
+ continue;
95
+ const row = {
96
+ base,
97
+ dexes: assets.map((a) => {
98
+ // All dex funding rates are already per-hour, same as hyperliquid
99
+ const ann = annualizeRate(a.fundingRate, "hyperliquid");
100
+ return { dex: a.dex, rate: a.fundingRate, annualizedPct: ann, markPrice: a.markPrice, oi: a.openInterest };
101
+ }),
102
+ };
103
+ dexAssets.push(row);
104
+ }
105
+ // Sort by max spread across dexes
106
+ dexAssets.sort((a, b) => {
107
+ const spreadA = Math.max(...a.dexes.map((d) => d.annualizedPct)) - Math.min(...a.dexes.map((d) => d.annualizedPct));
108
+ const spreadB = Math.max(...b.dexes.map((d) => d.annualizedPct)) - Math.min(...b.dexes.map((d) => d.annualizedPct));
109
+ return spreadB - spreadA;
110
+ });
111
+ }
112
+ catch {
113
+ // dex arb unavailable
114
+ }
115
+ const dexNames = [...dexNamesSet].sort();
116
+ return { opportunities, dexArb, dexAssets: dexAssets.slice(0, 30), dexNames, exchangeStatus };
117
+ }
118
+ /**
119
+ * Poll all exchanges and return a unified snapshot.
120
+ */
121
+ /** Poll market data for all exchanges (called on arb cycle, not every 5s) */
122
+ async function pollMarkets(exchanges) {
123
+ await Promise.allSettled(exchanges.map(async (ex) => {
124
+ try {
125
+ const markets = await ex.adapter.getMarkets();
126
+ cachedMarkets.set(ex.name, markets.slice(0, 10));
127
+ }
128
+ catch {
129
+ // keep previous cached data
130
+ }
131
+ }));
132
+ }
133
+ /** Build snapshot from WsFeedManager state (no REST calls needed) */
134
+ function buildSnapshotFromFeeds(feedMgr, exchanges) {
135
+ const states = feedMgr.getAllStates();
136
+ let exchangeData = [];
137
+ for (const ex of exchanges) {
138
+ const state = states.get(ex.name);
139
+ const emptyBal = { equity: "0", available: "0", marginUsed: "0", unrealizedPnl: "0" };
140
+ exchangeData.push({
141
+ name: ex.name,
142
+ balance: state?.balance ?? emptyBal,
143
+ positions: [...(state?.positions ?? [])], // copy — mergeAndTotal mutates
144
+ orders: [...(state?.orders ?? [])],
145
+ topMarkets: cachedMarkets.get(ex.name) ?? [],
146
+ });
147
+ }
148
+ return mergeAndTotal(exchangeData);
149
+ }
150
+ /** Legacy REST-based snapshot (used for /api/snapshot fallback) */
151
+ async function pollSnapshot(exchanges, feedMgr) {
152
+ // If feedMgr is available, use WS state directly (no API calls)
153
+ if (feedMgr) {
154
+ return buildSnapshotFromFeeds(feedMgr, exchanges);
155
+ }
156
+ const { withCache, TTL_ACCOUNT } = await import("../cache.js");
157
+ const emptyBalance = { equity: "0", available: "0", marginUsed: "0", unrealizedPnl: "0" };
158
+ const results = await Promise.allSettled(exchanges.map(async (ex) => {
159
+ const [balance, positions, orders] = await Promise.all([
160
+ withCache(`dash:${ex.name}:balance`, TTL_ACCOUNT, () => ex.adapter.getBalance()).catch(() => emptyBalance),
161
+ withCache(`dash:${ex.name}:positions`, TTL_ACCOUNT, () => ex.adapter.getPositions()).catch(() => []),
162
+ withCache(`dash:${ex.name}:orders`, TTL_ACCOUNT, () => ex.adapter.getOpenOrders()).catch(() => []),
163
+ ]);
164
+ return { name: ex.name, balance, positions, orders, topMarkets: cachedMarkets.get(ex.name) ?? [] };
165
+ }));
166
+ let exchangeData = results
167
+ .filter((r) => r.status === "fulfilled")
168
+ .map((r) => r.value);
169
+ return mergeAndTotal(exchangeData);
170
+ }
171
+ /** Merge HL dex entries + compute totals */
172
+ function mergeAndTotal(exchangeData) {
173
+ // Merge hl:* dex entries into main hyperliquid (same wallet, dex pools are subsets of main balance)
174
+ const hlEntry = exchangeData.find(e => e.name === "hyperliquid");
175
+ if (hlEntry) {
176
+ const dexEntries = exchangeData.filter(e => e.name.startsWith("hl:") && e.name !== "hyperliquid");
177
+ // Main accountValue already includes dex pool funds — DON'T add dex balances (would double-count).
178
+ // Only merge positions/orders, and attach dex breakdown for UI display.
179
+ const dexBalances = [];
180
+ let dexPnlSum = 0;
181
+ for (const dex of dexEntries) {
182
+ const dexName = dex.name.replace("hl:", "");
183
+ dexBalances.push({ name: dexName, balance: { ...dex.balance } });
184
+ hlEntry.positions.push(...dex.positions);
185
+ hlEntry.orders.push(...dex.orders);
186
+ dexPnlSum += Number(dex.balance.unrealizedPnl) || 0;
187
+ }
188
+ // Add dex unrealizedPnl to main HL balance (equity already includes dex funds, but PnL doesn't)
189
+ if (dexPnlSum !== 0) {
190
+ const hlPnl = Number(hlEntry.balance.unrealizedPnl) || 0;
191
+ hlEntry.balance = { ...hlEntry.balance, unrealizedPnl: String(hlPnl + dexPnlSum) };
192
+ }
193
+ if (dexEntries.length > 0) {
194
+ hlEntry.dexBalances = dexBalances;
195
+ }
196
+ exchangeData = exchangeData.filter(e => !e.name.startsWith("hl:") || e.name === "hyperliquid");
197
+ }
198
+ const totals = {
199
+ equity: 0,
200
+ available: 0,
201
+ marginUsed: 0,
202
+ unrealizedPnl: 0,
203
+ positionCount: 0,
204
+ orderCount: 0,
205
+ };
206
+ for (const ex of exchangeData) {
207
+ totals.equity += Number(ex.balance.equity) || 0;
208
+ totals.available += Number(ex.balance.available) || 0;
209
+ totals.marginUsed += Number(ex.balance.marginUsed) || 0;
210
+ totals.unrealizedPnl += Number(ex.balance.unrealizedPnl) || 0;
211
+ totals.positionCount += ex.positions.length;
212
+ totals.orderCount += ex.orders.length;
213
+ }
214
+ return { timestamp: new Date().toISOString(), exchanges: exchangeData, totals, arb: cachedArb };
215
+ }
216
+ /**
217
+ * Broadcast a message to all connected WebSocket clients.
218
+ */
219
+ function broadcast(wss, data) {
220
+ const msg = JSON.stringify(data);
221
+ for (const client of wss.clients) {
222
+ if (client.readyState === WebSocket.OPEN) {
223
+ client.send(msg);
224
+ }
225
+ }
226
+ }
227
+ /**
228
+ * Start the dashboard HTTP + WebSocket server.
229
+ */
230
+ export async function startDashboard(exchanges, opts = {}) {
231
+ const arbInterval = opts.arbInterval ?? 30000;
232
+ const requestedPort = opts.port ?? 3456;
233
+ const port = await findPort(requestedPort);
234
+ const html = getUI();
235
+ let feedMgr;
236
+ const server = createServer((req, res) => {
237
+ if (req.url === "/" || req.url === "/index.html") {
238
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
239
+ res.end(html);
240
+ }
241
+ else if (req.url === "/api/snapshot") {
242
+ pollSnapshot(exchanges, feedMgr).then((snap) => {
243
+ res.writeHead(200, { "Content-Type": "application/json" });
244
+ res.end(JSON.stringify(snap));
245
+ }).catch((err) => {
246
+ res.writeHead(500, { "Content-Type": "application/json" });
247
+ res.end(JSON.stringify({ error: String(err) }));
248
+ });
249
+ }
250
+ else {
251
+ res.writeHead(404);
252
+ res.end("Not found");
253
+ }
254
+ });
255
+ const wss = new WebSocketServer({ server });
256
+ const hasClients = () => wss.clients.size > 0;
257
+ // ── WS Feed Manager: real-time account data via exchange WS APIs ──
258
+ let broadcastTimer = null;
259
+ const BROADCAST_DEBOUNCE_MS = 1000;
260
+ feedMgr = new WsFeedManager(exchanges, {
261
+ onUpdate: (_exchange, _state) => {
262
+ // Debounce broadcasts: WS feeds fire rapidly, limit to 1/sec
263
+ if (broadcastTimer || !hasClients())
264
+ return;
265
+ broadcastTimer = setTimeout(() => {
266
+ broadcastTimer = null;
267
+ if (!hasClients())
268
+ return;
269
+ const snap = buildSnapshotFromFeeds(feedMgr, exchanges);
270
+ broadcast(wss, { type: "snapshot", data: snap });
271
+ }, BROADCAST_DEBOUNCE_MS);
272
+ },
273
+ signal: opts.signal,
274
+ });
275
+ // Send initial snapshot on connect
276
+ wss.on("connection", async (ws) => {
277
+ try {
278
+ const snap = await pollSnapshot(exchanges, feedMgr);
279
+ ws.send(JSON.stringify({ type: "snapshot", data: snap }));
280
+ }
281
+ catch {
282
+ // ignore
283
+ }
284
+ });
285
+ // Arb + market data: stays on REST polling (cross-exchange aggregation, no single WS covers it)
286
+ let arbTimer = null;
287
+ const startArbPolling = () => {
288
+ const pollArbAndMarkets = async () => {
289
+ try {
290
+ const [arbResult] = await Promise.allSettled([
291
+ pollArbData(),
292
+ pollMarkets(exchanges),
293
+ ]);
294
+ if (arbResult.status === "fulfilled") {
295
+ cachedArb = arbResult.value;
296
+ }
297
+ if (hasClients()) {
298
+ broadcast(wss, { type: "arb", data: cachedArb });
299
+ }
300
+ }
301
+ catch {
302
+ // ignore
303
+ }
304
+ };
305
+ pollArbAndMarkets(); // initial fetch
306
+ arbTimer = setInterval(pollArbAndMarkets, arbInterval);
307
+ };
308
+ // Handle abort signal
309
+ if (opts.signal) {
310
+ opts.signal.addEventListener("abort", () => {
311
+ if (broadcastTimer)
312
+ clearTimeout(broadcastTimer);
313
+ if (arbTimer)
314
+ clearInterval(arbTimer);
315
+ feedMgr?.close();
316
+ wss.close();
317
+ server.close();
318
+ }, { once: true });
319
+ }
320
+ return new Promise((resolve) => {
321
+ server.listen(port, async () => {
322
+ // Start WS feeds (connects to exchange WS APIs)
323
+ await feedMgr.start();
324
+ // Start arb REST polling (30s cycle)
325
+ startArbPolling();
326
+ resolve({
327
+ port,
328
+ close: () => {
329
+ if (broadcastTimer)
330
+ clearTimeout(broadcastTimer);
331
+ if (arbTimer)
332
+ clearInterval(arbTimer);
333
+ feedMgr?.close();
334
+ wss.close();
335
+ server.close();
336
+ },
337
+ });
338
+ });
339
+ });
340
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Dashboard frontend UI — single-file HTML/CSS/JS served inline.
3
+ */
4
+ export declare function getUI(): string;