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,538 @@
1
+ /**
2
+ * Dashboard frontend UI — single-file HTML/CSS/JS served inline.
3
+ */
4
+ export function getUI() {
5
+ return `<!DOCTYPE html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="UTF-8">
9
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
10
+ <title>perp-cli Dashboard</title>
11
+ <style>
12
+ :root {
13
+ --bg: #0d1117; --card: #161b22; --border: #30363d;
14
+ --text: #e6edf3; --muted: #8b949e; --green: #3fb950;
15
+ --red: #f85149; --blue: #58a6ff; --yellow: #d29922;
16
+ --cyan: #39d2c0; --purple: #bc8cff;
17
+ }
18
+ * { margin:0; padding:0; box-sizing:border-box; }
19
+ body { font-family: 'SF Mono', 'Fira Code', monospace; background: var(--bg); color: var(--text); min-height: 100vh; }
20
+ .header { display:flex; align-items:center; justify-content:space-between; padding:16px 24px; border-bottom:1px solid var(--border); }
21
+ .header h1 { font-size:18px; color:var(--cyan); }
22
+ .status { display:flex; align-items:center; gap:8px; font-size:13px; color:var(--muted); }
23
+ .status .dot { width:8px; height:8px; border-radius:50%; background:var(--green); }
24
+ .status .dot.off { background:var(--red); }
25
+ .container { padding:20px 24px; }
26
+
27
+ /* Nav tabs */
28
+ .nav { display:flex; gap:4px; margin-bottom:20px; border-bottom:1px solid var(--border); padding-bottom:0; }
29
+ .nav-tab { padding:8px 16px; cursor:pointer; font-size:13px; color:var(--muted); border-bottom:2px solid transparent; transition:all 0.15s; }
30
+ .nav-tab:hover { color:var(--text); }
31
+ .nav-tab.active { color:var(--cyan); border-bottom-color:var(--cyan); font-weight:600; }
32
+ .page { display:none; }
33
+ .page.active { display:block; }
34
+
35
+ /* Totals row */
36
+ .totals { display:grid; grid-template-columns:repeat(auto-fit, minmax(160px, 1fr)); gap:12px; margin-bottom:20px; }
37
+ .total-card { background:var(--card); border:1px solid var(--border); border-radius:8px; padding:14px 16px; }
38
+ .total-card .label { font-size:11px; text-transform:uppercase; color:var(--muted); margin-bottom:4px; letter-spacing:0.5px; }
39
+ .total-card .value { font-size:22px; font-weight:600; }
40
+ .total-card .value.green { color:var(--green); }
41
+ .total-card .value.red { color:var(--red); }
42
+
43
+ /* Exchange tabs */
44
+ .tabs { display:flex; gap:8px; margin-bottom:16px; }
45
+ .tab { padding:6px 14px; border-radius:6px; background:var(--card); border:1px solid var(--border); cursor:pointer; font-size:13px; color:var(--muted); transition:all 0.15s; }
46
+ .tab:hover { border-color:var(--cyan); color:var(--text); }
47
+ .tab.active { background:var(--cyan); color:var(--bg); border-color:var(--cyan); font-weight:600; }
48
+
49
+ /* Exchange panels */
50
+ .exchange-panel { display:none; }
51
+ .exchange-panel.active { display:block; }
52
+
53
+ /* Balance bar */
54
+ .balance-bar { display:grid; grid-template-columns:repeat(4, 1fr); gap:12px; margin-bottom:16px; }
55
+ .balance-item { background:var(--card); border:1px solid var(--border); border-radius:8px; padding:12px 14px; }
56
+ .balance-item .label { font-size:11px; text-transform:uppercase; color:var(--muted); margin-bottom:2px; }
57
+ .balance-item .val { font-size:17px; font-weight:500; }
58
+
59
+ /* Tables */
60
+ .section-title { font-size:14px; font-weight:600; margin:16px 0 8px; color:var(--cyan); }
61
+ table { width:100%; border-collapse:collapse; font-size:13px; }
62
+ thead th { text-align:left; padding:8px 10px; color:var(--muted); font-size:11px; text-transform:uppercase; border-bottom:1px solid var(--border); letter-spacing:0.5px; }
63
+ tbody td { padding:8px 10px; border-bottom:1px solid var(--border); }
64
+ tbody tr:hover { background:rgba(88,166,255,0.04); }
65
+ .side-long { color:var(--green); font-weight:600; }
66
+ .side-short, .side-sell { color:var(--red); font-weight:600; }
67
+ .side-buy { color:var(--green); font-weight:600; }
68
+ .pnl-pos { color:var(--green); }
69
+ .pnl-neg { color:var(--red); }
70
+ .empty-msg { color:var(--muted); font-size:13px; padding:12px 0; }
71
+
72
+ /* Arb-specific */
73
+ .spread-high { color:var(--green); font-weight:700; }
74
+ .spread-mid { color:var(--yellow); font-weight:600; }
75
+ .spread-low { color:var(--muted); }
76
+ .viability-A { color:var(--green); font-weight:700; }
77
+ .viability-B { color:var(--cyan); font-weight:600; }
78
+ .viability-C { color:var(--yellow); }
79
+ .viability-D { color:var(--muted); }
80
+ .exchange-status { display:inline-flex; align-items:center; gap:4px; font-size:12px; margin-right:12px; }
81
+ .exchange-status .dot-sm { width:6px; height:6px; border-radius:50%; display:inline-block; }
82
+ .dot-ok { background:var(--green); }
83
+ .dot-err { background:var(--red); }
84
+
85
+ /* Arb summary cards */
86
+ .arb-summary { display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:12px; margin-bottom:16px; }
87
+ .arb-card { background:var(--card); border:1px solid var(--border); border-radius:8px; padding:14px 16px; }
88
+ .arb-card .label { font-size:11px; text-transform:uppercase; color:var(--muted); margin-bottom:4px; }
89
+ .arb-card .value { font-size:20px; font-weight:600; }
90
+
91
+ /* Funding rate heatmap-style */
92
+ .rate-cell { font-weight:500; }
93
+ .rate-positive { color:var(--green); }
94
+ .rate-negative { color:var(--red); }
95
+ .rate-neutral { color:var(--muted); }
96
+
97
+ /* Sortable headers */
98
+ thead th.sortable { cursor:pointer; user-select:none; transition:color 0.15s; }
99
+ thead th.sortable:hover { color:var(--cyan); }
100
+
101
+ /* Spread bar */
102
+ .spread-bar { margin-top:3px; height:3px; width:100%; background:var(--border); border-radius:2px; overflow:hidden; }
103
+ .spread-bar-fill { height:100%; border-radius:2px; transition:width 0.3s; }
104
+
105
+ /* Event log */
106
+ .event-log { max-height:200px; overflow-y:auto; background:var(--card); border:1px solid var(--border); border-radius:8px; padding:10px 14px; font-size:12px; line-height:1.6; }
107
+ .event-log .event { border-bottom:1px solid var(--border); padding:3px 0; }
108
+ .event-log .event:last-child { border:none; }
109
+ .event-time { color:var(--muted); }
110
+ .event-type { font-weight:600; }
111
+ .event-type.warn { color:var(--yellow); }
112
+ .event-type.crit { color:var(--red); }
113
+
114
+ /* Dex filters */
115
+ .dex-filters { display:flex; gap:6px; margin:8px 0; flex-wrap:wrap; }
116
+ .dex-filter { padding:3px 10px; border-radius:4px; background:var(--card); border:1px solid var(--border); cursor:pointer; font-size:11px; color:var(--muted); transition:all 0.15s; }
117
+ .dex-filter:hover { border-color:var(--cyan); color:var(--text); }
118
+ .dex-filter.active { background:var(--cyan); color:var(--bg); border-color:var(--cyan); font-weight:600; }
119
+
120
+ .footer { padding:16px 24px; text-align:center; font-size:11px; color:var(--muted); border-top:1px solid var(--border); margin-top:20px; }
121
+
122
+ @media (max-width: 768px) {
123
+ .balance-bar { grid-template-columns:repeat(2, 1fr); }
124
+ .totals { grid-template-columns:repeat(2, 1fr); }
125
+ .arb-summary { grid-template-columns:1fr; }
126
+ }
127
+ </style>
128
+ </head>
129
+ <body>
130
+ <div class="header">
131
+ <h1>perp-cli dashboard</h1>
132
+ <div class="status">
133
+ <span class="dot" id="ws-dot"></span>
134
+ <span id="ws-status">connecting...</span>
135
+ <span id="last-update" style="margin-left:12px"></span>
136
+ </div>
137
+ </div>
138
+
139
+ <div class="container">
140
+ <!-- Top-level navigation -->
141
+ <div class="nav">
142
+ <div class="nav-tab active" data-page="portfolio">Portfolio</div>
143
+ <div class="nav-tab" data-page="arb">Arb Scanner</div>
144
+ <div class="nav-tab" data-page="dex-arb">DEX Arb</div>
145
+ <div class="nav-tab" data-page="events">Events</div>
146
+ </div>
147
+
148
+ <!-- ═══ Portfolio Page ═══ -->
149
+ <div class="page active" id="page-portfolio">
150
+ <div class="totals" id="totals"></div>
151
+ <div class="tabs" id="tabs"></div>
152
+ <div id="panels"></div>
153
+ </div>
154
+
155
+ <!-- ═══ Arb Scanner Page ═══ -->
156
+ <div class="page" id="page-arb">
157
+ <div class="arb-summary" id="arb-summary"></div>
158
+ <div id="arb-status" style="margin-bottom:12px"></div>
159
+ <div class="section-title">Cross-Exchange Funding Arb Opportunities</div>
160
+ <div id="arb-table"></div>
161
+ </div>
162
+
163
+ <!-- ═══ DEX Arb Page ═══ -->
164
+ <div class="page" id="page-dex-arb">
165
+ <div class="section-title">HIP-3 DEX Funding Rates</div>
166
+ <p style="color:var(--muted);font-size:12px;margin-bottom:12px">Funding rates across Hyperliquid deployed DEXs — sorted by max spread</p>
167
+ <div id="dex-rates-table"></div>
168
+
169
+ <div class="section-title" style="margin-top:24px">Cross-DEX Arb Opportunities</div>
170
+ <p style="color:var(--muted);font-size:12px;margin-bottom:12px">Best funding arb pairs across HIP-3 dexes (>10% annual spread)</p>
171
+ <div id="dex-arb-table"></div>
172
+ </div>
173
+
174
+ <!-- ═══ Events Page ═══ -->
175
+ <div class="page" id="page-events">
176
+ <div class="section-title">Event Log</div>
177
+ <div class="event-log" id="event-log" style="max-height:500px">
178
+ <div class="empty-msg">Waiting for events...</div>
179
+ </div>
180
+ </div>
181
+ </div>
182
+
183
+ <div class="footer">perp-cli v0.3.1 &mdash; live dashboard</div>
184
+
185
+ <script>
186
+ const $ = (s) => document.querySelector(s);
187
+ const $$ = (s) => document.querySelectorAll(s);
188
+
189
+ let ws;
190
+ let snapshot = null;
191
+ let arbData = null;
192
+ let activeExchange = null;
193
+ let activePage = 'portfolio';
194
+ const MAX_EVENTS = 100;
195
+ const events = [];
196
+ // Dex filter: which dex prefixes to show (null = show all)
197
+ let activeDexFilters = new Set(); // empty = show all
198
+ // Arb sort state
199
+ let arbSort = { col: 'spread', dir: 'desc' };
200
+
201
+ // ── Navigation ──
202
+ $$('.nav-tab').forEach(tab => {
203
+ tab.onclick = () => {
204
+ activePage = tab.dataset.page;
205
+ $$('.nav-tab').forEach(t => t.classList.remove('active'));
206
+ tab.classList.add('active');
207
+ $$('.page').forEach(p => p.classList.remove('active'));
208
+ $('#page-' + activePage).classList.add('active');
209
+ };
210
+ });
211
+
212
+ // ── WebSocket ──
213
+ function connect() {
214
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
215
+ ws = new WebSocket(proto + '//' + location.host);
216
+ ws.onopen = () => { $('#ws-dot').className = 'dot'; $('#ws-status').textContent = 'connected'; };
217
+ ws.onclose = () => { $('#ws-dot').className = 'dot off'; $('#ws-status').textContent = 'reconnecting...'; setTimeout(connect, 3000); };
218
+ ws.onerror = () => ws.close();
219
+ ws.onmessage = (e) => {
220
+ const msg = JSON.parse(e.data);
221
+ if (msg.type === 'snapshot') {
222
+ snapshot = msg.data;
223
+ if (msg.data.arb) arbData = msg.data.arb;
224
+ render();
225
+ }
226
+ if (msg.type === 'arb') {
227
+ arbData = msg.data;
228
+ renderArb();
229
+ renderDexRates();
230
+ renderDexArb();
231
+ }
232
+ };
233
+ }
234
+
235
+ // ── Helpers ──
236
+ function fmt(v, d=2) { const n=Number(v)||0; return n.toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d}); }
237
+ function pnlClass(v) { return Number(v)>=0?'pnl-pos':'pnl-neg'; }
238
+ function pnlSign(v) { const n=Number(v)||0; return n>=0?'+$'+fmt(Math.abs(n)):'-$'+fmt(Math.abs(n)); }
239
+ function rateClass(v) { const n=Number(v)||0; return n>0?'rate-positive':n<0?'rate-negative':'rate-neutral'; }
240
+ function spreadClass(v) { return v>=50?'spread-high':v>=20?'spread-mid':'spread-low'; }
241
+
242
+ // ── Portfolio Rendering ──
243
+ function renderTotals(t) {
244
+ const pnlCls = Number(t.unrealizedPnl)>=0?'green':'red';
245
+ $('#totals').innerHTML = [
246
+ {label:'Total Equity',value:'$'+fmt(t.equity),cls:''},
247
+ {label:'Available',value:'$'+fmt(t.available),cls:''},
248
+ {label:'Margin Used',value:'$'+fmt(t.marginUsed),cls:''},
249
+ {label:'Unrealized PnL',value:pnlSign(t.unrealizedPnl),cls:pnlCls},
250
+ {label:'Positions',value:t.positionCount,cls:''},
251
+ {label:'Open Orders',value:t.orderCount,cls:''},
252
+ ].map(c=>\`<div class="total-card"><div class="label">\${c.label}</div><div class="value \${c.cls}">\${c.value}</div></div>\`).join('');
253
+ }
254
+
255
+ function renderTabs(exchanges) {
256
+ if (!activeExchange && exchanges.length) activeExchange = exchanges[0].name;
257
+ $('#tabs').innerHTML = exchanges.map(ex=>\`<div class="tab \${ex.name===activeExchange?'active':''}" data-ex="\${ex.name}">\${ex.name}</div>\`).join('');
258
+ $$('.tab').forEach(tab => { tab.onclick=()=>{ activeExchange=tab.dataset.ex; render(); }; });
259
+ }
260
+
261
+ function getDex(symbol) {
262
+ // "xyz:CL" → "xyz", "BTC" → "hl"
263
+ return symbol.includes(':') ? symbol.split(':')[0] : 'hl';
264
+ }
265
+
266
+ function renderPanels(exchanges) {
267
+ $('#panels').innerHTML = exchanges.map(ex => {
268
+ const isActive = ex.name===activeExchange;
269
+ const isHL = ex.name === 'hyperliquid';
270
+
271
+ // For HL: detect unique dexes from positions/orders
272
+ let dexes = [];
273
+ let filteredPositions = ex.positions;
274
+ let filteredOrders = ex.orders;
275
+ if (isHL) {
276
+ const dexSet = new Set();
277
+ ex.positions.forEach(p => dexSet.add(getDex(p.symbol)));
278
+ ex.orders.forEach(o => dexSet.add(getDex(o.symbol)));
279
+ dexes = ['all', ...Array.from(dexSet).sort()];
280
+ if (activeDexFilters.size > 0) {
281
+ filteredPositions = ex.positions.filter(p => activeDexFilters.has(getDex(p.symbol)));
282
+ filteredOrders = ex.orders.filter(o => activeDexFilters.has(getDex(o.symbol)));
283
+ }
284
+ }
285
+
286
+ const dexFilterHtml = isHL && dexes.length > 2 ? \`<div class="dex-filters">\${dexes.map(d =>
287
+ \`<div class="dex-filter \${d==='all' ? (activeDexFilters.size===0?'active':'') : (activeDexFilters.has(d)?'active':'')}" data-dex="\${d}">\${d}</div>\`
288
+ ).join('')}</div>\` : '';
289
+
290
+ // Dex balance breakdown (dex pools are subsets of main balance, not additive)
291
+ const dexBal = ex.dexBalances || null;
292
+ const dexBreakdown = (field) => {
293
+ if (!dexBal || dexBal.length < 1) return '';
294
+ return '<div style="font-size:10px;color:var(--muted);margin-top:2px">' +
295
+ dexBal.map(d => d.name + ': $' + fmt(Number(d.balance[field]))).join(' · ') + '</div>';
296
+ };
297
+
298
+ return \`<div class="exchange-panel \${isActive?'active':''}" id="panel-\${ex.name}">
299
+ <div class="balance-bar">
300
+ <div class="balance-item"><div class="label">Equity</div><div class="val">$\${fmt(ex.balance.equity)}</div>\${dexBreakdown('equity')}</div>
301
+ <div class="balance-item"><div class="label">Available</div><div class="val">$\${fmt(ex.balance.available)}</div>\${dexBreakdown('available')}</div>
302
+ <div class="balance-item"><div class="label">Margin Used</div><div class="val">$\${fmt(ex.balance.marginUsed)}</div>\${dexBreakdown('marginUsed')}</div>
303
+ <div class="balance-item"><div class="label">Unrealized PnL</div><div class="val \${pnlClass(ex.balance.unrealizedPnl)}">\${pnlSign(ex.balance.unrealizedPnl)}</div>\${dexBreakdown('unrealizedPnl')}</div>
304
+ </div>
305
+ <div class="section-title">Positions (\${filteredPositions.length}\${isHL && activeDexFilters.size>0 ? '/'+ex.positions.length : ''})</div>
306
+ \${dexFilterHtml}
307
+ \${filteredPositions.length?\`<table><thead><tr>\${isHL?'<th>DEX</th>':''}<th>Symbol</th><th>Side</th><th>Size</th><th>Entry</th><th>Mark</th><th>Liq</th><th>Value</th><th>PnL</th><th>ROE%</th><th>Lev</th></tr></thead><tbody>\${filteredPositions.map(p=>{
308
+ const dex = getDex(p.symbol);
309
+ const sym = p.symbol.includes(':') ? p.symbol.split(':').slice(1).join(':') : p.symbol;
310
+ const notional = Math.abs(Number(p.size)) * Number(p.markPrice);
311
+ const margin = p.leverage > 0 ? notional / p.leverage : notional;
312
+ const roe = margin > 0 ? (Number(p.unrealizedPnl) / margin * 100) : 0;
313
+ return \`<tr>\${isHL?\`<td style="color:var(--purple);font-size:11px">\${dex}</td>\`:''}<td>\${sym}</td><td class="side-\${p.side}">\${p.side.toUpperCase()}</td><td>\${p.size}</td><td>$\${fmt(p.entryPrice)}</td><td>$\${fmt(p.markPrice)}</td><td>\${p.liquidationPrice==='N/A'?'N/A':'$'+fmt(p.liquidationPrice)}</td><td style="color:var(--muted)">$\${fmt(notional)}</td><td class="\${pnlClass(p.unrealizedPnl)}">\${pnlSign(p.unrealizedPnl)}</td><td class="\${pnlClass(p.unrealizedPnl)}">\${roe>=0?'+':''}\${fmt(roe,1)}%</td><td>\${p.leverage}x</td></tr>\`;
314
+ }).join('')}</tbody></table>\`:'<div class="empty-msg">No open positions</div>'}
315
+ <div class="section-title">Open Orders (\${filteredOrders.length})</div>
316
+ \${filteredOrders.length?\`<table><thead><tr>\${isHL?'<th>DEX</th>':''}<th>Symbol</th><th>Side</th><th>Type</th><th>Price</th><th>Size</th><th>Filled</th><th>Status</th></tr></thead><tbody>\${filteredOrders.map(o=>{
317
+ const dex = getDex(o.symbol);
318
+ const sym = o.symbol.includes(':') ? o.symbol.split(':').slice(1).join(':') : o.symbol;
319
+ return \`<tr>\${isHL?\`<td style="color:var(--purple);font-size:11px">\${dex}</td>\`:''}<td>\${sym}</td><td class="side-\${o.side}">\${o.side.toUpperCase()}</td><td>\${o.type}</td><td>$\${fmt(o.price)}</td><td>\${o.size}</td><td>\${o.filled}</td><td>\${o.status}</td></tr>\`;
320
+ }).join('')}</tbody></table>\`:'<div class="empty-msg">No open orders</div>'}
321
+ <div class="section-title">Markets (Top 10)</div>
322
+ \${ex.topMarkets.length?\`<table><thead><tr><th>Symbol</th><th>Mark</th><th>Index</th><th>Funding</th><th>24h Vol</th><th>OI</th><th>Max Lev</th></tr></thead><tbody>\${ex.topMarkets.map(m=>{const fr=Number(m.fundingRate);return\`<tr><td>\${m.symbol}</td><td>$\${fmt(m.markPrice)}</td><td>$\${fmt(m.indexPrice)}</td><td class="\${rateClass(fr)}">\${(fr*100).toFixed(4)}%</td><td>$\${fmt(m.volume24h,0)}</td><td>$\${fmt(m.openInterest,0)}</td><td>\${m.maxLeverage}x</td></tr>\`;}).join('')}</tbody></table>\`:'<div class="empty-msg">No market data</div>'}
323
+ </div>\`;
324
+ }).join('');
325
+
326
+ // Bind dex filter clicks
327
+ $$('.dex-filter').forEach(btn => {
328
+ btn.onclick = () => {
329
+ const dex = btn.dataset.dex;
330
+ if (dex === 'all') {
331
+ activeDexFilters.clear();
332
+ } else {
333
+ if (activeDexFilters.has(dex)) {
334
+ activeDexFilters.delete(dex);
335
+ } else {
336
+ activeDexFilters.add(dex);
337
+ }
338
+ }
339
+ render();
340
+ };
341
+ });
342
+ }
343
+
344
+ // ── Arb Rendering ──
345
+ function renderArb() {
346
+ if (!arbData) { $('#arb-table').innerHTML='<div class="empty-msg">Loading arb data...</div>'; return; }
347
+
348
+ // Status
349
+ const statusHtml = Object.entries(arbData.exchangeStatus||{}).map(([ex,st])=>
350
+ \`<span class="exchange-status"><span class="dot-sm \${st==='ok'?'dot-ok':'dot-err'}"></span>\${ex}</span>\`
351
+ ).join('');
352
+ $('#arb-status').innerHTML = statusHtml;
353
+
354
+ // Summary cards
355
+ const opps = arbData.opportunities || [];
356
+ const bestSpread = opps.length ? opps[0].spreadAnnual : 0;
357
+ const bestDaily = opps.length ? Math.max(...opps.map(o=>o.estHourlyUsd*24)) : 0;
358
+ const totalOpps = opps.length;
359
+ const highSpread = opps.filter(o=>o.spreadAnnual>=50).length;
360
+ $('#arb-summary').innerHTML = [
361
+ {label:'Best Spread',value:fmt(bestSpread,1)+'%',cls:bestSpread>=50?'green':''},
362
+ {label:'Best Daily ($1k)',value:'$'+fmt(bestDaily,2),cls:bestDaily>1?'green':''},
363
+ {label:'Opportunities (>5%)',value:totalOpps,cls:''},
364
+ {label:'High Spread (>50%)',value:highSpread,cls:highSpread>0?'green':''},
365
+ ].map(c=>\`<div class="arb-card"><div class="label">\${c.label}</div><div class="value \${c.cls}">\${c.value}</div></div>\`).join('');
366
+
367
+ if (!opps.length) { $('#arb-table').innerHTML='<div class="empty-msg">No arb opportunities found (>5% annual spread)</div>'; return; }
368
+
369
+ // Sort
370
+ const sorted = [...opps].sort((a,b) => {
371
+ let va, vb;
372
+ switch(arbSort.col) {
373
+ case 'symbol': return arbSort.dir==='asc'?a.symbol.localeCompare(b.symbol):b.symbol.localeCompare(a.symbol);
374
+ case 'spread': va=a.spreadAnnual; vb=b.spreadAnnual; break;
375
+ case 'income': va=a.estHourlyUsd; vb=b.estHourlyUsd; break;
376
+ default: va=a.spreadAnnual; vb=b.spreadAnnual;
377
+ }
378
+ return arbSort.dir==='asc' ? va-vb : vb-va;
379
+ });
380
+
381
+ const maxSpread = Math.max(...opps.map(o=>o.spreadAnnual), 1);
382
+ const sortH = (col, label) => {
383
+ const active = arbSort.col===col;
384
+ const arrow = active ? (arbSort.dir==='desc'?' \\u2193':' \\u2191') : '';
385
+ return \`<th class="sortable" data-sort="\${col}" style="\${active?'color:var(--cyan)':''}">\${label}\${arrow}</th>\`;
386
+ };
387
+
388
+ $('#arb-table').innerHTML = \`<table id="arb-tbl">
389
+ <thead><tr>
390
+ \${sortH('symbol','Symbol')}
391
+ \${sortH('spread','Spread')}
392
+ <th>Strategy</th>
393
+ <th>Mark</th>
394
+ \${sortH('income','Est Income ($1k)')}
395
+ \${['pacifica','hyperliquid','lighter'].map(e=>\`<th>\${e.slice(0,3).toUpperCase()}</th>\`).join('')}
396
+ </tr></thead>
397
+ <tbody>\${sorted.map(o => {
398
+ const rateMap = {};
399
+ o.rates.forEach(r => rateMap[r.exchange] = r);
400
+ const daily = o.estHourlyUsd * 24;
401
+ const monthly = daily * 30;
402
+ const barW = Math.min(100, (o.spreadAnnual / maxSpread) * 100);
403
+ const barColor = o.spreadAnnual>=50?'var(--green)':o.spreadAnnual>=20?'var(--yellow)':'var(--muted)';
404
+ const bestMark = o.rates.reduce((best,r) => r.markPrice>0?r.markPrice:best, 0);
405
+ return \`<tr>
406
+ <td style="font-weight:600">\${o.symbol}</td>
407
+ <td>
408
+ <div class="\${spreadClass(o.spreadAnnual)}">\${fmt(o.spreadAnnual,1)}%</div>
409
+ <div class="spread-bar"><div class="spread-bar-fill" style="width:\${barW}%;background:\${barColor}"></div></div>
410
+ </td>
411
+ <td>
412
+ <div style="font-size:12px"><span class="side-long">Long</span> <span style="color:var(--cyan);font-weight:600">\${o.longExchange.slice(0,3).toUpperCase()}</span></div>
413
+ <div style="font-size:12px"><span class="side-short">Short</span> <span style="color:var(--cyan);font-weight:600">\${o.shortExchange.slice(0,3).toUpperCase()}</span></div>
414
+ </td>
415
+ <td style="color:var(--muted);font-size:12px">\${bestMark>0?'$'+fmt(bestMark):'-'}</td>
416
+ <td>
417
+ <div style="font-weight:600;color:\${daily>0?'var(--green)':'var(--muted)'}">\${daily>0?'$'+fmt(daily,2)+'/d':'-'}</div>
418
+ <div style="font-size:11px;color:var(--muted)">\${monthly>0?'$'+fmt(monthly,0)+'/mo':''}</div>
419
+ </td>
420
+ \${['pacifica','hyperliquid','lighter'].map(ex => {
421
+ const r = rateMap[ex];
422
+ if (!r) return '<td style="text-align:center;color:var(--muted)">-</td>';
423
+ return \`<td style="text-align:center">
424
+ <div class="rate-cell \${rateClass(r.annualizedPct)}" style="font-size:12px">\${(r.hourlyRate*100).toFixed(4)}%/h</div>
425
+ <div style="font-size:10px;color:var(--muted)">\${fmt(r.annualizedPct,1)}% ann.</div>
426
+ </td>\`;
427
+ }).join('')}
428
+ </tr>\`;
429
+ }).join('')}</tbody>
430
+ </table>\`;
431
+
432
+ // Bind sort clicks
433
+ $$('#arb-tbl th.sortable').forEach(th => {
434
+ th.onclick = () => {
435
+ const col = th.dataset.sort;
436
+ if (arbSort.col===col) { arbSort.dir = arbSort.dir==='desc'?'asc':'desc'; }
437
+ else { arbSort.col=col; arbSort.dir='desc'; }
438
+ renderArb();
439
+ };
440
+ });
441
+ }
442
+
443
+ function renderDexRates() {
444
+ if (!arbData) { $('#dex-rates-table').innerHTML='<div class="empty-msg">Loading HIP-3 rates...</div>'; return; }
445
+ const assets = arbData.dexAssets || [];
446
+ const dexNames = arbData.dexNames || [];
447
+ if (!assets.length) { $('#dex-rates-table').innerHTML='<div class="empty-msg">No HIP-3 DEX data available</div>'; return; }
448
+ $('#dex-rates-table').innerHTML = \`<table>
449
+ <thead><tr><th>Asset</th><th>Spread</th>\${dexNames.map(d=>\`<th>\${d}</th>\`).join('')}</tr></thead>
450
+ <tbody>\${assets.map(a => {
451
+ const rateMap = {};
452
+ a.dexes.forEach(d => rateMap[d.dex] = d);
453
+ const rates = a.dexes.map(d=>d.annualizedPct);
454
+ const spread = rates.length>=2 ? Math.max(...rates)-Math.min(...rates) : 0;
455
+ return \`<tr>
456
+ <td>\${a.base}</td>
457
+ <td class="\${spreadClass(spread)}">\${fmt(spread,1)}%</td>
458
+ \${dexNames.map(dn => {
459
+ const d = rateMap[dn];
460
+ if (!d) return '<td class="rate-neutral" style="font-size:11px">-</td>';
461
+ const title = '$'+fmt(d.markPrice)+' | OI: $'+fmt(d.oi,0);
462
+ return \`<td class="rate-cell \${rateClass(d.annualizedPct)}" title="\${title}" style="font-size:11px;cursor:help">\${fmt(d.annualizedPct,1)}%</td>\`;
463
+ }).join('')}
464
+ </tr>\`;
465
+ }).join('')}</tbody>
466
+ </table>\`;
467
+ }
468
+
469
+ function renderDexArb() {
470
+ if (!arbData) { $('#dex-arb-table').innerHTML='<div class="empty-msg">Loading DEX arb data...</div>'; return; }
471
+ const dex = arbData.dexArb || [];
472
+ if (!dex.length) { $('#dex-arb-table').innerHTML='<div class="empty-msg">No DEX arb opportunities found (>10% annual spread)</div>'; return; }
473
+ $('#dex-arb-table').innerHTML = \`<table>
474
+ <thead><tr><th>Underlying</th><th>Spread (Ann.)</th><th>Long (low rate)</th><th>Short (high rate)</th><th>Price Gap</th><th>Viability</th></tr></thead>
475
+ <tbody>\${dex.map(d=>\`<tr>
476
+ <td>\${d.underlying}</td>
477
+ <td class="\${spreadClass(d.annualSpread)}">\${fmt(d.annualSpread,1)}%</td>
478
+ <td class="side-long">\${d.longDex}</td>
479
+ <td class="side-short">\${d.shortDex}</td>
480
+ <td>\${fmt(d.priceGapPct,2)}%</td>
481
+ <td class="viability-\${d.viability}">\${d.viability}</td>
482
+ </tr>\`).join('')}</tbody>
483
+ </table>\`;
484
+ }
485
+
486
+ // ── Events ──
487
+ function addEvent(type, exchange, data) {
488
+ const time = new Date().toLocaleTimeString();
489
+ const isWarn = type.includes('warning');
490
+ const isCrit = type.includes('margin_call') || type.includes('critical');
491
+ events.unshift({time,type,exchange,data,isWarn,isCrit});
492
+ if (events.length > MAX_EVENTS) events.pop();
493
+ renderEvents();
494
+ }
495
+
496
+ function renderEvents() {
497
+ const el = $('#event-log');
498
+ if (!events.length) { el.innerHTML='<div class="empty-msg">Waiting for events...</div>'; return; }
499
+ el.innerHTML = events.map(e => {
500
+ const cls = e.isCrit?'crit':e.isWarn?'warn':'';
501
+ return \`<div class="event"><span class="event-time">\${e.time}</span> <span class="event-type \${cls}">[\${e.type}]</span> <span>\${e.exchange}</span> <span style="color:var(--muted)">\${JSON.stringify(e.data).slice(0,120)}</span></div>\`;
502
+ }).join('');
503
+ }
504
+
505
+ let prevSnapshot = null;
506
+ function detectEvents(snap) {
507
+ if (!prevSnapshot) { prevSnapshot = snap; return; }
508
+ for (const ex of snap.exchanges) {
509
+ const prev = prevSnapshot.exchanges.find(e => e.name === ex.name);
510
+ if (!prev) continue;
511
+ const prevSyms = new Set(prev.positions.map(p => p.symbol));
512
+ const currSyms = new Set(ex.positions.map(p => p.symbol));
513
+ for (const p of ex.positions) { if (!prevSyms.has(p.symbol)) addEvent('position_opened', ex.name, {symbol:p.symbol,side:p.side,size:p.size}); }
514
+ for (const p of prev.positions) { if (!currSyms.has(p.symbol)) addEvent('position_closed', ex.name, {symbol:p.symbol,side:p.side}); }
515
+ const eqDelta = Math.abs(Number(ex.balance.equity) - Number(prev.balance.equity));
516
+ if (eqDelta > 0.01) addEvent('balance_update', ex.name, {equity:ex.balance.equity,delta:eqDelta.toFixed(2)});
517
+ }
518
+ prevSnapshot = snap;
519
+ }
520
+
521
+ // ── Main render ──
522
+ function render() {
523
+ if (!snapshot) return;
524
+ renderTotals(snapshot.totals);
525
+ renderTabs(snapshot.exchanges);
526
+ renderPanels(snapshot.exchanges);
527
+ renderArb();
528
+ renderDexRates();
529
+ renderDexArb();
530
+ $('#last-update').textContent = new Date(snapshot.timestamp).toLocaleTimeString();
531
+ detectEvents(snapshot);
532
+ }
533
+
534
+ connect();
535
+ </script>
536
+ </body>
537
+ </html>`;
538
+ }
@@ -0,0 +1,29 @@
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 type { ExchangeBalance, ExchangePosition, ExchangeOrder } from "../exchanges/interface.js";
9
+ import type { DashboardExchange } from "./server.js";
10
+ export interface WsFeedState {
11
+ balance: ExchangeBalance;
12
+ positions: ExchangePosition[];
13
+ orders: ExchangeOrder[];
14
+ lastUpdate: number;
15
+ mode: "ws" | "rest" | "connecting";
16
+ }
17
+ export interface WsFeedManagerOpts {
18
+ onUpdate: (exchange: string, state: WsFeedState) => void;
19
+ signal?: AbortSignal;
20
+ }
21
+ export declare class WsFeedManager {
22
+ private feeds;
23
+ private onUpdate;
24
+ constructor(exchanges: DashboardExchange[], opts: WsFeedManagerOpts);
25
+ start(): Promise<void>;
26
+ getState(exchange: string): WsFeedState | null;
27
+ getAllStates(): Map<string, WsFeedState>;
28
+ close(): void;
29
+ }