perp-cli 0.9.8 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/README.md +30 -25
  2. package/dist/_stderr-filter.d.ts +0 -0
  3. package/dist/_stderr-filter.js +30 -0
  4. package/dist/agent-wallet/expiry.d.ts +15 -0
  5. package/dist/agent-wallet/expiry.js +26 -0
  6. package/dist/agent-wallet/lighter-keystore.d.ts +34 -0
  7. package/dist/agent-wallet/lighter-keystore.js +232 -0
  8. package/dist/agent-wallet/passphrase.d.ts +13 -0
  9. package/dist/agent-wallet/passphrase.js +54 -0
  10. package/dist/agent-wallet/signing-strategy.d.ts +67 -0
  11. package/dist/agent-wallet/signing-strategy.js +51 -0
  12. package/dist/agent-wallet/store.d.ts +37 -0
  13. package/dist/agent-wallet/store.js +172 -0
  14. package/dist/api/public/_http.d.ts +15 -0
  15. package/dist/api/public/_http.js +24 -0
  16. package/dist/api/public/hyperliquid.js +36 -24
  17. package/dist/api/public/lighter.d.ts +7 -1
  18. package/dist/api/public/lighter.js +85 -37
  19. package/dist/api/public/pacifica.js +47 -18
  20. package/dist/bot/engine.js +23 -4
  21. package/dist/bot/presets.js +3 -3
  22. package/dist/bot/strategies/funding-arb-strategy.d.ts +0 -23
  23. package/dist/bot/strategies/funding-arb-strategy.js +41 -176
  24. package/dist/bot/strategies/funding-auto.js +2 -2
  25. package/dist/bridge-engine.js +102 -49
  26. package/dist/cli-spec.js +1 -2
  27. package/dist/commands/account.js +2 -500
  28. package/dist/commands/agent.d.ts +8 -2
  29. package/dist/commands/agent.js +1903 -366
  30. package/dist/commands/alerts.js +3 -1
  31. package/dist/commands/arb/index.js +37 -7
  32. package/dist/commands/arb-auto.js +44 -11
  33. package/dist/commands/arb.js +83 -34
  34. package/dist/commands/bot.d.ts +1 -1
  35. package/dist/commands/bot.js +20 -16
  36. package/dist/commands/bridge.d.ts +1 -1
  37. package/dist/commands/bridge.js +4 -4
  38. package/dist/commands/funds.d.ts +1 -1
  39. package/dist/commands/funds.js +18 -68
  40. package/dist/commands/jobs.d.ts +1 -1
  41. package/dist/commands/jobs.js +2 -2
  42. package/dist/commands/manage.d.ts +1 -1
  43. package/dist/commands/manage.js +12 -84
  44. package/dist/commands/plan.d.ts +1 -1
  45. package/dist/commands/plan.js +2 -2
  46. package/dist/commands/portfolio.d.ts +19 -3
  47. package/dist/commands/portfolio.js +747 -163
  48. package/dist/commands/rebalance.d.ts +1 -1
  49. package/dist/commands/rebalance.js +18 -12
  50. package/dist/commands/risk.js +2 -6
  51. package/dist/commands/settings.js +30 -53
  52. package/dist/commands/trade.js +16 -15
  53. package/dist/commands/wallet.d.ts +2 -1
  54. package/dist/commands/wallet.js +1122 -176
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +55 -9
  57. package/dist/errors.d.ts +64 -0
  58. package/dist/errors.js +82 -2
  59. package/dist/event-stream.js +12 -1
  60. package/dist/exchanges/aster-typed-data.d.ts +156 -0
  61. package/dist/exchanges/aster-typed-data.js +189 -0
  62. package/dist/exchanges/aster.d.ts +60 -11
  63. package/dist/exchanges/aster.js +278 -136
  64. package/dist/exchanges/hyperliquid.d.ts +14 -0
  65. package/dist/exchanges/hyperliquid.js +88 -34
  66. package/dist/exchanges/lighter-spot.js +19 -5
  67. package/dist/exchanges/lighter.d.ts +66 -0
  68. package/dist/exchanges/lighter.js +213 -31
  69. package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
  70. package/dist/exchanges/pacifica-typed-data.js +74 -0
  71. package/dist/exchanges/pacifica.d.ts +29 -1
  72. package/dist/exchanges/pacifica.js +94 -6
  73. package/dist/funding/rates.js +59 -21
  74. package/dist/guardrail/contracts.d.ts +14 -0
  75. package/dist/guardrail/contracts.js +72 -0
  76. package/dist/guardrail/perp-guardrail.d.ts +21 -0
  77. package/dist/guardrail/perp-guardrail.js +145 -0
  78. package/dist/index.d.ts +1 -1
  79. package/dist/index.js +307 -362
  80. package/dist/jobs.js +3 -1
  81. package/dist/mcp-server.js +92 -88
  82. package/dist/position-history.js +47 -4
  83. package/dist/settings.d.ts +69 -3
  84. package/dist/settings.js +3 -2
  85. package/dist/signer/index.d.ts +2 -0
  86. package/dist/signer/index.js +2 -0
  87. package/dist/signer/ows-evm.d.ts +32 -0
  88. package/dist/signer/ows-evm.js +86 -0
  89. package/dist/signer/ows-loader.d.ts +2 -0
  90. package/dist/signer/ows-loader.js +6 -0
  91. package/dist/signer/ows-solana.d.ts +25 -0
  92. package/dist/signer/ows-solana.js +56 -0
  93. package/dist/smart-order.d.ts +8 -2
  94. package/dist/smart-order.js +46 -11
  95. package/dist/strategies/funding-arb.js +29 -8
  96. package/dist/trade-validator.js +30 -5
  97. package/dist/utils.d.ts +10 -0
  98. package/dist/utils.js +19 -0
  99. package/package.json +4 -2
  100. package/skills/perp-cli/SKILL.md +23 -14
  101. package/skills/perp-cli/references/agent-operations.md +6 -4
  102. package/skills/perp-cli/references/commands.md +55 -34
  103. package/skills/perp-cli/scripts/preflight.sh +4 -4
  104. package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
  105. package/dist/__tests__/alert-logic.test.d.ts +0 -1
  106. package/dist/__tests__/alert-logic.test.js +0 -107
  107. package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
  108. package/dist/__tests__/arb-auto-3dex.test.js +0 -397
  109. package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
  110. package/dist/__tests__/arb-history-stats.test.js +0 -176
  111. package/dist/__tests__/arb-logic.test.d.ts +0 -1
  112. package/dist/__tests__/arb-logic.test.js +0 -84
  113. package/dist/__tests__/arb-manage.test.d.ts +0 -1
  114. package/dist/__tests__/arb-manage.test.js +0 -253
  115. package/dist/__tests__/arb-new-features.test.d.ts +0 -1
  116. package/dist/__tests__/arb-new-features.test.js +0 -457
  117. package/dist/__tests__/arb-sizing.test.d.ts +0 -1
  118. package/dist/__tests__/arb-sizing.test.js +0 -48
  119. package/dist/__tests__/arb-state.test.d.ts +0 -1
  120. package/dist/__tests__/arb-state.test.js +0 -284
  121. package/dist/__tests__/arb-userflow.test.d.ts +0 -1
  122. package/dist/__tests__/arb-userflow.test.js +0 -945
  123. package/dist/__tests__/arb-utils.test.d.ts +0 -1
  124. package/dist/__tests__/arb-utils.test.js +0 -264
  125. package/dist/__tests__/bot-conditions.test.d.ts +0 -1
  126. package/dist/__tests__/bot-conditions.test.js +0 -341
  127. package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
  128. package/dist/__tests__/client-id-tracker.test.js +0 -137
  129. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
  130. package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
  131. package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
  132. package/dist/__tests__/commands/order-intent.test.js +0 -600
  133. package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
  134. package/dist/__tests__/commands/trade-commands.test.js +0 -821
  135. package/dist/__tests__/config.test.d.ts +0 -1
  136. package/dist/__tests__/config.test.js +0 -86
  137. package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
  138. package/dist/__tests__/cross-chain-margin.test.js +0 -287
  139. package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
  140. package/dist/__tests__/dex-asset-map.test.js +0 -191
  141. package/dist/__tests__/errors.test.d.ts +0 -1
  142. package/dist/__tests__/errors.test.js +0 -110
  143. package/dist/__tests__/event-stream.test.d.ts +0 -1
  144. package/dist/__tests__/event-stream.test.js +0 -276
  145. package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
  146. package/dist/__tests__/exchanges/interface.test.js +0 -132
  147. package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
  148. package/dist/__tests__/exchanges/mock-adapter.js +0 -137
  149. package/dist/__tests__/execution-log.test.d.ts +0 -1
  150. package/dist/__tests__/execution-log.test.js +0 -106
  151. package/dist/__tests__/funding-calc.test.d.ts +0 -1
  152. package/dist/__tests__/funding-calc.test.js +0 -71
  153. package/dist/__tests__/funding-history.test.d.ts +0 -1
  154. package/dist/__tests__/funding-history.test.js +0 -343
  155. package/dist/__tests__/funding-rates.test.d.ts +0 -1
  156. package/dist/__tests__/funding-rates.test.js +0 -342
  157. package/dist/__tests__/funding.test.d.ts +0 -1
  158. package/dist/__tests__/funding.test.js +0 -173
  159. package/dist/__tests__/gap-logic.test.d.ts +0 -1
  160. package/dist/__tests__/gap-logic.test.js +0 -43
  161. package/dist/__tests__/hip3-dex.test.d.ts +0 -1
  162. package/dist/__tests__/hip3-dex.test.js +0 -234
  163. package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
  164. package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
  165. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
  166. package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
  167. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
  168. package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
  169. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
  170. package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
  171. package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
  172. package/dist/__tests__/integration/bridge.integration.test.js +0 -309
  173. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
  174. package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
  175. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
  176. package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
  177. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
  178. package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
  179. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
  180. package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
  181. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
  182. package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
  183. package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
  184. package/dist/__tests__/integration/lighter.integration.test.js +0 -53
  185. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
  186. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
  187. package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
  188. package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
  189. package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
  190. package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
  191. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
  192. package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
  193. package/dist/__tests__/liquidity.test.d.ts +0 -1
  194. package/dist/__tests__/liquidity.test.js +0 -225
  195. package/dist/__tests__/plan-executor.test.d.ts +0 -1
  196. package/dist/__tests__/plan-executor.test.js +0 -314
  197. package/dist/__tests__/position-history.test.d.ts +0 -1
  198. package/dist/__tests__/position-history.test.js +0 -367
  199. package/dist/__tests__/retry.test.d.ts +0 -1
  200. package/dist/__tests__/retry.test.js +0 -310
  201. package/dist/__tests__/risk-assessment.test.d.ts +0 -1
  202. package/dist/__tests__/risk-assessment.test.js +0 -288
  203. package/dist/__tests__/security-adversarial.test.d.ts +0 -1
  204. package/dist/__tests__/security-adversarial.test.js +0 -574
  205. package/dist/__tests__/strategies.test.d.ts +0 -1
  206. package/dist/__tests__/strategies.test.js +0 -539
  207. package/dist/__tests__/trade-execution.test.d.ts +0 -1
  208. package/dist/__tests__/trade-execution.test.js +0 -129
  209. package/dist/__tests__/trade-validator.test.d.ts +0 -1
  210. package/dist/__tests__/trade-validator.test.js +0 -655
  211. package/dist/__tests__/utils.test.d.ts +0 -1
  212. package/dist/__tests__/utils.test.js +0 -76
  213. package/dist/arb/reconcile.d.ts +0 -28
  214. package/dist/arb/reconcile.js +0 -162
  215. package/dist/commands/alert.d.ts +0 -2
  216. package/dist/commands/alert.js +0 -421
  217. package/dist/commands/analytics.d.ts +0 -3
  218. package/dist/commands/analytics.js +0 -311
  219. package/dist/commands/arb-manage.d.ts +0 -5
  220. package/dist/commands/arb-manage.js +0 -5
  221. package/dist/commands/dashboard.d.ts +0 -3
  222. package/dist/commands/dashboard.js +0 -176
  223. package/dist/commands/deposit.d.ts +0 -4
  224. package/dist/commands/deposit.js +0 -573
  225. package/dist/commands/dex.d.ts +0 -3
  226. package/dist/commands/dex.js +0 -120
  227. package/dist/commands/env.d.ts +0 -2
  228. package/dist/commands/env.js +0 -136
  229. package/dist/commands/funding.d.ts +0 -3
  230. package/dist/commands/funding.js +0 -554
  231. package/dist/commands/gap.d.ts +0 -2
  232. package/dist/commands/gap.js +0 -305
  233. package/dist/commands/health.d.ts +0 -2
  234. package/dist/commands/health.js +0 -67
  235. package/dist/commands/multileg.d.ts +0 -16
  236. package/dist/commands/multileg.js +0 -174
  237. package/dist/commands/pnl.d.ts +0 -12
  238. package/dist/commands/pnl.js +0 -243
  239. package/dist/commands/run.d.ts +0 -3
  240. package/dist/commands/run.js +0 -202
  241. package/dist/commands/stream.d.ts +0 -5
  242. package/dist/commands/stream.js +0 -200
  243. package/dist/commands/withdraw.d.ts +0 -3
  244. package/dist/commands/withdraw.js +0 -187
  245. package/dist/exchanges/edgex.d.ts +0 -62
  246. package/dist/exchanges/edgex.js +0 -552
  247. package/dist/exchanges/nado.d.ts +0 -52
  248. package/dist/exchanges/nado.js +0 -223
  249. package/dist/funding/index.d.ts +0 -3
  250. package/dist/funding/index.js +0 -3
  251. package/dist/retry.d.ts +0 -74
  252. package/dist/retry.js +0 -129
  253. package/dist/ws/market-feed.d.ts +0 -74
  254. package/dist/ws/market-feed.js +0 -227
@@ -1,196 +1,780 @@
1
1
  import chalk from "chalk";
2
- import { printJson, jsonOk, makeTable, formatUsd, formatPnl, withJsonErrors } from "../utils.js";
3
- import { assessRisk } from "../risk.js";
4
- const EXCHANGES = ["pacifica", "hyperliquid", "lighter"];
5
- async function fetchExchangeSnapshot(name, getAdapter) {
2
+ import { makeTable, formatUsd, formatPnl, printJson, jsonOk, jsonError, withJsonErrors } from "../utils.js";
3
+ import { isDexCapable } from "../exchanges/capabilities.js";
4
+ // ── Types ──────────────────────────────────────────────────────────────────
5
+ const EXCHANGES = ["pacifica", "hyperliquid", "lighter", "aster"];
6
+ // ── Helpers ────────────────────────────────────────────────────────────────
7
+ function exAbbr(e) {
8
+ return e === "pacifica" ? "PAC"
9
+ : e === "hyperliquid" ? "HL"
10
+ : e === "lighter" ? "LT"
11
+ : e === "aster" ? "AST"
12
+ : e.toUpperCase().slice(0, 3);
13
+ }
14
+ async function readStdinJson() {
15
+ return new Promise((resolve, reject) => {
16
+ let data = "";
17
+ process.stdin.setEncoding("utf8");
18
+ process.stdin.on("data", chunk => { data += chunk; });
19
+ process.stdin.on("end", () => {
20
+ const trimmed = data.trim();
21
+ if (!trimmed)
22
+ return resolve({});
23
+ try {
24
+ const parsed = JSON.parse(trimmed);
25
+ resolve(parsed);
26
+ }
27
+ catch (err) {
28
+ reject(err instanceof Error ? err : new Error(String(err)));
29
+ }
30
+ });
31
+ process.stdin.on("error", reject);
32
+ });
33
+ }
34
+ function parseList(input) {
35
+ if (!input)
36
+ return [];
37
+ return input.split(",").map(s => s.trim()).filter(Boolean);
38
+ }
39
+ function validateExchanges(list) {
40
+ const lower = list.map(s => s.toLowerCase());
41
+ const unknown = lower.filter(e => !EXCHANGES.includes(e));
42
+ if (unknown.length > 0) {
43
+ throw new Error(`Unknown exchange: ${unknown.join(", ")}. Valid: ${EXCHANGES.join(", ")}`);
44
+ }
45
+ return lower;
46
+ }
47
+ /** Resolve global -e flag from program (only counts when the user typed it). */
48
+ function explicitExchange(program) {
49
+ if (program.getOptionValueSource?.("exchange") === "cli") {
50
+ const ex = program.opts().exchange;
51
+ return ex ? ex.toLowerCase() : undefined;
52
+ }
53
+ return undefined;
54
+ }
55
+ function resolveOptions(program, flags, stdinOpts) {
56
+ // 1) Build exchange list. Precedence: stdin > --exchanges > -e > all.
57
+ let exchangeList;
58
+ let scope = "all";
59
+ if (stdinOpts.exchanges && stdinOpts.exchanges.length > 0) {
60
+ exchangeList = validateExchanges(stdinOpts.exchanges);
61
+ }
62
+ else if (stdinOpts.exchange) {
63
+ exchangeList = validateExchanges([stdinOpts.exchange]);
64
+ scope = "single";
65
+ }
66
+ else if (flags.exchanges) {
67
+ exchangeList = validateExchanges(parseList(flags.exchanges));
68
+ }
69
+ else {
70
+ const ex = explicitExchange(program);
71
+ if (ex) {
72
+ exchangeList = validateExchanges([ex]);
73
+ scope = "single";
74
+ }
75
+ else {
76
+ exchangeList = [...EXCHANGES];
77
+ }
78
+ }
79
+ // If only 1 entry and we got there via -e or stdin.exchange, scope is single.
80
+ // Otherwise multi-exchange aggregate.
81
+ if (exchangeList.length === 1 && scope === "all" && (stdinOpts.exchange || explicitExchange(program))) {
82
+ scope = "single";
83
+ }
84
+ // 2) Mode flags. stdin overrides CLI.
85
+ const arb = stdinOpts.arb ?? !!flags.arb;
86
+ const health = stdinOpts.health ?? !!flags.health;
87
+ let watch = false;
88
+ let watchIntervalMs = 5000;
89
+ if (stdinOpts.watch !== undefined) {
90
+ if (typeof stdinOpts.watch === "object" && stdinOpts.watch !== null) {
91
+ watch = true;
92
+ if (typeof stdinOpts.watch.intervalMs === "number")
93
+ watchIntervalMs = stdinOpts.watch.intervalMs;
94
+ }
95
+ else if (stdinOpts.watch === true) {
96
+ watch = true;
97
+ }
98
+ }
99
+ else if (flags.watch !== undefined && flags.watch !== false) {
100
+ watch = true;
101
+ if (typeof flags.watch === "string" && flags.watch.length > 0) {
102
+ const n = Number(flags.watch);
103
+ if (Number.isFinite(n) && n > 0)
104
+ watchIntervalMs = n;
105
+ }
106
+ }
107
+ let serve = false;
108
+ let servePort = 3456;
109
+ let pollIntervalMs = 5000;
110
+ if (stdinOpts.serve !== undefined) {
111
+ if (typeof stdinOpts.serve === "object" && stdinOpts.serve !== null) {
112
+ serve = true;
113
+ if (typeof stdinOpts.serve.port === "number")
114
+ servePort = stdinOpts.serve.port;
115
+ if (typeof stdinOpts.serve.intervalMs === "number")
116
+ pollIntervalMs = stdinOpts.serve.intervalMs;
117
+ }
118
+ else if (stdinOpts.serve === true) {
119
+ serve = true;
120
+ }
121
+ }
122
+ else if (flags.serve !== undefined && flags.serve !== false) {
123
+ serve = true;
124
+ if (typeof flags.serve === "string" && flags.serve.length > 0) {
125
+ const n = parseInt(flags.serve, 10);
126
+ if (Number.isFinite(n) && n > 0)
127
+ servePort = n;
128
+ }
129
+ }
130
+ if (flags.interval !== undefined) {
131
+ const n = parseInt(flags.interval, 10);
132
+ if (Number.isFinite(n) && n > 0) {
133
+ pollIntervalMs = n;
134
+ if (watchIntervalMs === 5000)
135
+ watchIntervalMs = n;
136
+ }
137
+ }
138
+ const dexList = stdinOpts.dex ?? parseList(flags.dex);
139
+ const autoDex = stdinOpts.autoDex ?? (flags.autoDex !== false);
140
+ return {
141
+ exchangeList,
142
+ scope,
143
+ arb,
144
+ health,
145
+ watch,
146
+ watchIntervalMs,
147
+ serve,
148
+ servePort,
149
+ pollIntervalMs,
150
+ dexList,
151
+ autoDex,
152
+ };
153
+ }
154
+ // ── Per-exchange snapshot fetch ────────────────────────────────────────────
155
+ async function fetchExchangeEntry(exName, getAdapter) {
6
156
  try {
7
- const adapter = await getAdapter(name);
8
- const [balance, positions, orders] = await Promise.all([
157
+ const adapter = await getAdapter(exName);
158
+ const [bal, positionsRaw, orders, fundingPayments] = await Promise.all([
9
159
  adapter.getBalance(),
10
160
  adapter.getPositions(),
11
161
  adapter.getOpenOrders(),
162
+ adapter.getFundingPayments(200).catch(() => []),
12
163
  ]);
164
+ // Spot balances (only HL + LT). Pacifica/Aster are perp-only.
165
+ let spot = [];
166
+ let isUnified = false;
167
+ try {
168
+ if (exName === "hyperliquid") {
169
+ const { HyperliquidSpotAdapter } = await import("../exchanges/hyperliquid-spot.js");
170
+ const { HyperliquidAdapter } = await import("../exchanges/hyperliquid.js");
171
+ const hlSpot = new HyperliquidSpotAdapter(adapter);
172
+ await hlSpot.init();
173
+ const [raw, markets] = await Promise.all([hlSpot.getSpotBalances(), hlSpot.getSpotMarkets()]);
174
+ const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
175
+ const strip = (t) => t.replace(/-SPOT$/i, "").toUpperCase();
176
+ spot = raw.filter(b => Number(b.total) > 0).map(b => {
177
+ const base = strip(b.token);
178
+ return {
179
+ token: b.token,
180
+ total: b.total,
181
+ available: b.available,
182
+ held: b.held,
183
+ valueUsd: base === "USDC" ? Number(b.total) : (priceMap.get(base) ?? 0) * Number(b.total),
184
+ };
185
+ });
186
+ isUnified = !isDexCapable(adapter) || !adapter.dex;
187
+ }
188
+ else if (exName === "lighter") {
189
+ const { LighterAdapter } = await import("../exchanges/lighter.js");
190
+ const { LighterSpotAdapter } = await import("../exchanges/lighter-spot.js");
191
+ const ltSpot = new LighterSpotAdapter(adapter);
192
+ await ltSpot.init();
193
+ const [raw, markets] = await Promise.all([ltSpot.getSpotBalances(), ltSpot.getSpotMarkets()]);
194
+ const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
195
+ spot = raw.filter(b => Number(b.total) > 0).map(b => ({
196
+ token: b.token,
197
+ total: b.total,
198
+ available: b.available,
199
+ held: b.held,
200
+ valueUsd: b.token === "USDC" || b.token === "USDC_SPOT"
201
+ ? Number(b.total)
202
+ : (priceMap.get(b.token.toUpperCase()) ?? 0) * Number(b.total),
203
+ }));
204
+ }
205
+ }
206
+ catch { /* spot not available */ }
207
+ const cutoff24h = Date.now() - 24 * 60 * 60 * 1000;
208
+ const recent = fundingPayments.filter(f => f.time >= cutoff24h);
209
+ const funding24h = recent.reduce((sum, f) => sum + Number(f.payment), 0);
210
+ // For unified accounts (HL), USDC spot is already in perp equity.
211
+ // Account value = perp equity + non-USDC spot; for non-unified, equity + all spot.
212
+ const nonUsdcSpotValue = isUnified
213
+ ? spot.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC").reduce((s, b) => s + b.valueUsd, 0)
214
+ : spot.reduce((s, b) => s + b.valueUsd, 0);
13
215
  return {
14
- exchange: name,
216
+ name: exName,
15
217
  connected: true,
16
- balance,
17
- positions,
218
+ isUnified,
219
+ perp: {
220
+ equity: bal.equity,
221
+ available: bal.available,
222
+ marginUsed: bal.marginUsed,
223
+ unrealizedPnl: bal.unrealizedPnl,
224
+ },
225
+ spot,
226
+ positions: positionsRaw.map(p => ({
227
+ symbol: p.symbol,
228
+ side: p.side,
229
+ size: p.size,
230
+ entryPrice: p.entryPrice,
231
+ markPrice: p.markPrice,
232
+ unrealizedPnl: p.unrealizedPnl,
233
+ leverage: p.leverage,
234
+ })),
18
235
  openOrders: orders.length,
236
+ funding24h,
237
+ totalAccountValueUsd: Number(bal.equity) + nonUsdcSpotValue,
238
+ error: null,
19
239
  };
20
240
  }
21
241
  catch (err) {
22
242
  return {
23
- exchange: name,
243
+ name: exName,
24
244
  connected: false,
25
- balance: null,
245
+ isUnified: false,
246
+ perp: null,
247
+ spot: [],
26
248
  positions: [],
27
249
  openOrders: 0,
250
+ funding24h: 0,
251
+ totalAccountValueUsd: 0,
28
252
  error: err instanceof Error ? err.message : String(err),
29
253
  };
30
254
  }
31
255
  }
32
- function buildSummary(snapshots) {
33
- let totalEquity = 0;
34
- let totalAvailable = 0;
35
- let totalMarginUsed = 0;
36
- let totalUnrealizedPnl = 0;
37
- let totalPositions = 0;
38
- let totalOpenOrders = 0;
39
- const allPositions = [];
40
- for (const snap of snapshots) {
41
- if (snap.balance) {
42
- totalEquity += Number(snap.balance.equity);
43
- totalAvailable += Number(snap.balance.available);
44
- totalMarginUsed += Number(snap.balance.marginUsed);
45
- totalUnrealizedPnl += Number(snap.balance.unrealizedPnl);
46
- }
47
- totalPositions += snap.positions.length;
48
- totalOpenOrders += snap.openOrders;
49
- for (const pos of snap.positions) {
50
- allPositions.push({ ...pos, exchange: snap.exchange });
51
- }
52
- }
53
- // Risk metrics
54
- const marginUtilization = totalEquity > 0 ? (totalMarginUsed / totalEquity) * 100 : 0;
55
- let largestPosition = null;
56
- for (const pos of allPositions) {
57
- const notional = Math.abs(Number(pos.size) * Number(pos.markPrice));
58
- if (!largestPosition || notional > largestPosition.notional) {
59
- largestPosition = { symbol: pos.symbol, exchange: pos.exchange, notional };
60
- }
61
- }
62
- const exchangeConcentration = snapshots
63
- .filter(s => s.balance && Number(s.balance.equity) > 0)
64
- .map(s => ({
65
- exchange: s.exchange,
66
- pct: totalEquity > 0 ? (Number(s.balance.equity) / totalEquity) * 100 : 0,
67
- }))
68
- .sort((a, b) => b.pct - a.pct);
69
- // Full risk assessment (reuses same data, no extra API calls)
70
- const riskBalances = snapshots
71
- .filter(s => s.balance)
72
- .map(s => ({ exchange: s.exchange, balance: s.balance }));
73
- const riskPositions = allPositions.map(p => ({ exchange: p.exchange, position: p }));
74
- const assessment = assessRisk(riskBalances, riskPositions);
256
+ function buildTotals(entries) {
257
+ let equity = 0;
258
+ let spotValueUsd = 0;
259
+ let marginUsed = 0;
260
+ let funding24h = 0;
261
+ let accountValueUsd = 0;
262
+ for (const e of entries) {
263
+ if (e.perp) {
264
+ equity += Number(e.perp.equity);
265
+ marginUsed += Number(e.perp.marginUsed);
266
+ }
267
+ funding24h += e.funding24h;
268
+ // Non-USDC spot value (for unified accounts USDC is already in equity)
269
+ const exSpot = e.isUnified
270
+ ? e.spot.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
271
+ : e.spot;
272
+ const exSpotVal = exSpot.reduce((s, b) => s + b.valueUsd, 0);
273
+ spotValueUsd += exSpotVal;
274
+ accountValueUsd += e.totalAccountValueUsd;
275
+ }
276
+ // Use accountValueUsd (equity + non-USDC spot) as the denominator so risk
277
+ // matches what the legacy `status` command reported.
278
+ const denom = accountValueUsd > 0 ? accountValueUsd : equity;
279
+ const marginPct = denom > 0 ? (marginUsed / denom) * 100 : 0;
280
+ const level = marginPct < 30 ? "LOW" : marginPct < 60 ? "MEDIUM" : "HIGH";
75
281
  return {
76
- totalEquity,
77
- totalAvailable,
78
- totalMarginUsed,
79
- totalUnrealizedPnl,
80
- totalPositions,
81
- totalOpenOrders,
82
- exchanges: snapshots,
83
- positions: allPositions,
84
- riskMetrics: {
85
- marginUtilization,
86
- largestPosition,
87
- exchangeConcentration,
88
- },
89
- risk: {
90
- level: assessment.level,
91
- canTrade: assessment.canTrade,
92
- violations: assessment.violations,
93
- },
282
+ totals: { equity, spotValueUsd, accountValueUsd, marginUsed, marginPct, funding24h },
283
+ risk: { level, marginPct },
284
+ };
285
+ }
286
+ // ── Health check (no balance fetches) ──────────────────────────────────────
287
+ async function runHealth(exchangeList) {
288
+ const { pingPacifica, pingHyperliquid, pingLighter } = await import("../shared-api.js");
289
+ const pingAster = async () => {
290
+ const start = Date.now();
291
+ try {
292
+ const res = await fetch("https://fapi.asterdex.com/fapi/v1/time");
293
+ return { ok: res.ok, latencyMs: Date.now() - start, status: res.status };
294
+ }
295
+ catch {
296
+ return { ok: false, latencyMs: Date.now() - start, status: 0 };
297
+ }
298
+ };
299
+ const pingMap = {
300
+ pacifica: pingPacifica,
301
+ hyperliquid: pingHyperliquid,
302
+ lighter: pingLighter,
303
+ aster: pingAster,
304
+ };
305
+ const exchanges = {};
306
+ await Promise.all(exchangeList.map(async (ex) => {
307
+ const fn = pingMap[ex];
308
+ if (!fn) {
309
+ exchanges[ex] = "error";
310
+ return;
311
+ }
312
+ try {
313
+ const r = await fn();
314
+ exchanges[ex] = r.ok ? "ok" : "error";
315
+ }
316
+ catch {
317
+ exchanges[ex] = "error";
318
+ }
319
+ }));
320
+ const anyDown = Object.values(exchanges).some(v => v === "error");
321
+ return { exchanges, anyDown };
322
+ }
323
+ // ── Arb top-5 ──────────────────────────────────────────────────────────────
324
+ async function fetchArbTop() {
325
+ const { fetchAllFundingRates, TOP_SYMBOLS } = await import("../funding-rates.js");
326
+ const { saveFundingSnapshot, getHistoricalAverages } = await import("../funding-history.js");
327
+ let snapshot;
328
+ try {
329
+ snapshot = await fetchAllFundingRates({ symbols: TOP_SYMBOLS, minSpread: 0 });
330
+ }
331
+ catch {
332
+ snapshot = null;
333
+ }
334
+ if (!snapshot)
335
+ return [];
336
+ // Save snapshot for sparkline history (matches old status behavior).
337
+ try {
338
+ const allRates = snapshot.symbols.flatMap(s => s.rates);
339
+ if (allRates.length > 0)
340
+ saveFundingSnapshot(allRates);
341
+ }
342
+ catch { /* ignore */ }
343
+ const top = snapshot.symbols
344
+ .filter(s => s.maxSpreadAnnual >= 5)
345
+ .sort((a, b) => b.maxSpreadAnnual - a.maxSpreadAnnual)
346
+ .slice(0, 5);
347
+ const syms = top.map(a => a.symbol);
348
+ const avgs = syms.length > 0 ? getHistoricalAverages(syms, ["hyperliquid", "pacifica", "lighter"]) : new Map();
349
+ return top.map(a => {
350
+ const bestEx = a.rates.find(r => r.exchange === "hyperliquid")?.exchange ?? a.rates[0]?.exchange ?? "hyperliquid";
351
+ const avg = avgs.get(`${a.symbol}:${bestEx}`);
352
+ return {
353
+ symbol: a.symbol,
354
+ spreadAnnual: a.maxSpreadAnnual,
355
+ direction: `${exAbbr(a.shortExchange)}>${exAbbr(a.longExchange)}`,
356
+ longExchange: a.longExchange,
357
+ shortExchange: a.shortExchange,
358
+ avg24h: avg?.avg24h != null ? Math.abs(avg.avg24h) * 8760 * 100 : null,
359
+ avg7d: avg?.avg7d != null ? Math.abs(avg.avg7d) * 8760 * 100 : null,
360
+ };
361
+ });
362
+ }
363
+ // ── Snapshot envelope builder ──────────────────────────────────────────────
364
+ async function buildSnapshotEnvelope(resolved, version, getAdapterForExchange, mode = "snapshot") {
365
+ const entries = await Promise.all(resolved.exchangeList.map(ex => fetchExchangeEntry(ex, getAdapterForExchange)));
366
+ const { totals, risk } = buildTotals(entries);
367
+ const data = {
368
+ version,
369
+ scope: resolved.scope,
370
+ mode,
371
+ exchanges: entries,
372
+ totals,
373
+ risk,
94
374
  };
375
+ if (resolved.arb) {
376
+ data.arb = { topOpportunities: await fetchArbTop() };
377
+ }
378
+ return data;
95
379
  }
96
- export function registerPortfolioCommands(program, getAdapterForExchange, isJson) {
380
+ // ── Text rendering ─────────────────────────────────────────────────────────
381
+ function renderSnapshotText(data) {
382
+ const v = data.version;
383
+ console.log(chalk.cyan.bold(`\n perp-cli v${v}`) + chalk.gray(` — ${data.exchanges.filter(e => e.connected).length} exchanges connected\n`));
384
+ // Balances column (per-exchange equity bars)
385
+ const balLines = [];
386
+ balLines.push(chalk.white.bold(" Balances"));
387
+ for (const e of data.exchanges) {
388
+ if (!e.connected) {
389
+ balLines.push(` ${chalk.gray(exAbbr(e.name).padEnd(4))} ${chalk.red("disconnected")}`);
390
+ continue;
391
+ }
392
+ const equity = e.perp ? Number(e.perp.equity) : 0;
393
+ const margin = e.perp ? Number(e.perp.marginUsed) : 0;
394
+ const exSpot = e.isUnified
395
+ ? e.spot.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
396
+ : e.spot;
397
+ const exSpotVal = exSpot.reduce((s, b) => s + b.valueUsd, 0);
398
+ const total = equity + exSpotVal;
399
+ const usagePct = equity > 0 ? (margin / equity) * 100 : 0;
400
+ const barFull = Math.min(20, Math.round(usagePct / 5));
401
+ const usageColor = usagePct < 30 ? chalk.green : usagePct < 60 ? chalk.yellow : chalk.red;
402
+ const bar = usageColor("█".repeat(barFull)) + chalk.gray("░".repeat(20 - barFull));
403
+ balLines.push(` ${chalk.white.bold(exAbbr(e.name).padEnd(4))} $${formatUsd(total).padEnd(9)} ${bar} ${usagePct.toFixed(0)}% used`);
404
+ }
405
+ // Spot holdings (non-USDC tokens with value across all exchanges)
406
+ const allSpot = [];
407
+ for (const e of data.exchanges) {
408
+ const exSpot = e.isUnified
409
+ ? e.spot.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
410
+ : e.spot;
411
+ for (const b of exSpot) {
412
+ const tk = b.token.replace(/[-_]SPOT$/i, "").toUpperCase();
413
+ if (tk === "USDC")
414
+ continue;
415
+ allSpot.push({ exchange: e.name, token: b.token, total: b.total, valueUsd: b.valueUsd });
416
+ }
417
+ }
418
+ if (allSpot.length > 0) {
419
+ balLines.push("");
420
+ balLines.push(chalk.white.bold(" Spot Holdings"));
421
+ for (const b of allSpot) {
422
+ const token = b.token.replace(/-SPOT$/i, "");
423
+ balLines.push(` ${chalk.gray(exAbbr(b.exchange).padEnd(4))} ${chalk.white.bold(token.padEnd(6))} ${b.total.padEnd(12)} ${chalk.gray(`$${formatUsd(b.valueUsd)}`)}`);
424
+ }
425
+ }
426
+ const riskColor = data.risk.level === "LOW" ? chalk.green : data.risk.level === "MEDIUM" ? chalk.yellow : chalk.red;
427
+ balLines.push(` ${"─".repeat(44)}`);
428
+ balLines.push(` ${chalk.cyan.bold("Total")} ${chalk.cyan.bold(`$${formatUsd(data.totals.accountValueUsd)}`.padEnd(9))} Risk: ${riskColor(data.risk.level)}`);
429
+ // Arb column (only when --arb passed)
430
+ const arbLines = [];
431
+ if (data.arb) {
432
+ arbLines.push(chalk.white.bold(" Top Arb Opportunities"));
433
+ if (data.arb.topOpportunities.length > 0) {
434
+ arbLines.push(chalk.gray(" " + "".padEnd(8) + "now".padEnd(9) + "24h".padEnd(7) + "7d".padEnd(7)));
435
+ for (const a of data.arb.topOpportunities) {
436
+ const spreadColor = a.spreadAnnual >= 100 ? chalk.green.bold : a.spreadAnnual >= 30 ? chalk.green : chalk.yellow;
437
+ const avg24h = a.avg24h != null ? `${a.avg24h.toFixed(0)}%` : "-";
438
+ const avg7d = a.avg7d != null ? `${a.avg7d.toFixed(0)}%` : "-";
439
+ arbLines.push(` ${chalk.white.bold(a.symbol.padEnd(8))} ${spreadColor(`${a.spreadAnnual.toFixed(1)}%`.padEnd(9))}${chalk.gray(avg24h.padEnd(7))}${chalk.gray(avg7d.padEnd(7))}${chalk.gray(a.direction)}`);
440
+ }
441
+ }
442
+ else {
443
+ arbLines.push(chalk.gray(" No opportunities above 5%"));
444
+ }
445
+ }
446
+ // Layout: side-by-side if wide enough AND we have arb data, else stacked.
447
+ const stripAnsi = (s) => s.replace(/\[[0-9;]*m/g, "");
448
+ const termW = process.stdout.columns || 80;
449
+ const SIDE_BY_SIDE_MIN = 100;
450
+ if (data.arb && termW >= SIDE_BY_SIDE_MIN) {
451
+ const LEFT_W = 46;
452
+ const RIGHT_W = 48;
453
+ const maxLines = Math.max(balLines.length, arbLines.length);
454
+ console.log(` ${"┌"}${"─".repeat(LEFT_W)}${"┬"}${"─".repeat(RIGHT_W)}${"┐"}`);
455
+ for (let i = 0; i < maxLines; i++) {
456
+ const left = balLines[i] ?? "";
457
+ const right = arbLines[i] ?? "";
458
+ const leftPad = LEFT_W - stripAnsi(left).length;
459
+ const rightPad = RIGHT_W - stripAnsi(right).length;
460
+ console.log(` │${left}${" ".repeat(Math.max(0, leftPad))}│${right}${" ".repeat(Math.max(0, rightPad))}│`);
461
+ }
462
+ console.log(` ${"└"}${"─".repeat(LEFT_W)}${"┴"}${"─".repeat(RIGHT_W)}${"┘"}`);
463
+ }
464
+ else {
465
+ const boxW = Math.min(termW - 4, 74);
466
+ const printBox = (lines) => {
467
+ console.log(` ${"┌"}${"─".repeat(boxW)}${"┐"}`);
468
+ for (const line of lines) {
469
+ const pad = boxW - stripAnsi(line).length;
470
+ console.log(` │${line}${" ".repeat(Math.max(0, pad))}│`);
471
+ }
472
+ console.log(` ${"└"}${"─".repeat(boxW)}${"┘"}`);
473
+ };
474
+ printBox(balLines);
475
+ if (arbLines.length > 0) {
476
+ console.log();
477
+ printBox(arbLines);
478
+ }
479
+ }
480
+ // Positions table
481
+ const allPositions = [];
482
+ for (const e of data.exchanges) {
483
+ for (const p of e.positions)
484
+ allPositions.push({ ...p, exchange: e.name });
485
+ }
486
+ if (allPositions.length > 0) {
487
+ console.log(chalk.white.bold("\n Positions"));
488
+ const posRows = allPositions.map(p => {
489
+ const sideColor = p.side === "long" ? chalk.green : chalk.red;
490
+ const notional = Math.abs(Number(p.size) * Number(p.markPrice));
491
+ const levStr = p.leverage > 0 ? `${p.leverage}x` : "-";
492
+ const warn = p.leverage >= 5 ? chalk.red(" ⚠") : "";
493
+ return [
494
+ chalk.white.bold(p.symbol.replace("-PERP", "")),
495
+ chalk.gray(exAbbr(p.exchange)),
496
+ sideColor(p.side.toUpperCase()),
497
+ p.size,
498
+ `$${formatUsd(p.entryPrice)}→$${formatUsd(p.markPrice)}`,
499
+ formatPnl(p.unrealizedPnl),
500
+ `$${formatUsd(notional)}`,
501
+ levStr + warn,
502
+ ];
503
+ });
504
+ console.log(makeTable(["Symbol", "Ex", "Side", "Size", "Entry→Mark", "uPnL", "Notional", "Lev"], posRows));
505
+ }
506
+ else {
507
+ console.log(chalk.gray("\n No open positions.\n"));
508
+ }
509
+ }
510
+ function renderHealthText(h) {
511
+ console.log(chalk.cyan.bold("\n Exchange Health Check\n"));
512
+ const rows = Object.entries(h.exchanges).map(([ex, status]) => {
513
+ const statusIcon = status === "ok" ? chalk.green("OK") : chalk.red("DOWN");
514
+ return [chalk.white.bold(ex), statusIcon];
515
+ });
516
+ console.log(makeTable(["Exchange", "Status"], rows));
517
+ const overall = h.anyDown ? chalk.red("ISSUES DETECTED") : chalk.green("ALL HEALTHY");
518
+ console.log(`\n Overall: ${overall}\n`);
519
+ }
520
+ // ── Watch mode ─────────────────────────────────────────────────────────────
521
+ async function runWatch(resolved, version, getAdapterForExchange, isJsonMode) {
522
+ let stopped = false;
523
+ process.on("SIGINT", () => {
524
+ stopped = true;
525
+ process.exit(0);
526
+ });
527
+ while (!stopped) {
528
+ const data = await buildSnapshotEnvelope(resolved, version, getAdapterForExchange, "watch");
529
+ if (isJsonMode) {
530
+ // NDJSON-style: one envelope per tick, no padding.
531
+ console.log(JSON.stringify(jsonOk(data)));
532
+ }
533
+ else {
534
+ // Clear screen + redraw.
535
+ process.stdout.write("");
536
+ renderSnapshotText(data);
537
+ if (resolved.health)
538
+ renderHealthText(await runHealth(resolved.exchangeList));
539
+ }
540
+ await new Promise(r => setTimeout(r, resolved.watchIntervalMs));
541
+ }
542
+ }
543
+ // ── Serve mode (delegates to dashboard infra) ──────────────────────────────
544
+ const KNOWN_HIP3_DEXES = ["xyz", "flx", "hyna", "km", "cash", "vntl"];
545
+ const HL_INFO_URL = "https://api.hyperliquid.xyz/info";
546
+ async function checkDexPositions(address, dex) {
547
+ try {
548
+ const res = await fetch(HL_INFO_URL, {
549
+ method: "POST",
550
+ headers: { "Content-Type": "application/json" },
551
+ body: JSON.stringify({ type: "clearinghouseState", user: address, dex }),
552
+ });
553
+ const state = await res.json();
554
+ const positions = (state?.assetPositions ?? []);
555
+ return positions.filter(p => {
556
+ const pos = (p.position ?? p);
557
+ return Number(pos.szi ?? 0) !== 0;
558
+ }).length;
559
+ }
560
+ catch {
561
+ return 0;
562
+ }
563
+ }
564
+ async function autoDetectHip3Dexes(address, getHLAdapterForDex, verbose) {
565
+ const checks = await Promise.all(KNOWN_HIP3_DEXES.map(async (dex) => ({
566
+ dex,
567
+ count: await checkDexPositions(address, dex),
568
+ })));
569
+ const activeDexes = checks.filter(c => c.count > 0);
570
+ const results = [];
571
+ for (const { dex, count } of activeDexes) {
572
+ try {
573
+ const adapter = await getHLAdapterForDex(dex);
574
+ results.push({ name: `hl:${dex}`, adapter });
575
+ if (verbose)
576
+ console.log(chalk.green(` ✓ hl:${dex} (HIP-3, ${count} position${count > 1 ? "s" : ""})`));
577
+ }
578
+ catch { /* skip */ }
579
+ }
580
+ return results;
581
+ }
582
+ async function runServe(resolved, getAdapterForExchange, isJsonMode, getHLAdapterForDex) {
583
+ const { startDashboard } = await import("../dashboard/index.js");
584
+ if (isJsonMode) {
585
+ const exchanges = [];
586
+ for (const name of resolved.exchangeList) {
587
+ try {
588
+ const adapter = await getAdapterForExchange(name);
589
+ exchanges.push({ name, adapter });
590
+ }
591
+ catch { /* skip */ }
592
+ }
593
+ if (getHLAdapterForDex) {
594
+ for (const dex of resolved.dexList) {
595
+ try {
596
+ const adapter = await getHLAdapterForDex(dex);
597
+ exchanges.push({ name: `hl:${dex}`, adapter });
598
+ }
599
+ catch { /* skip */ }
600
+ }
601
+ if (resolved.autoDex && !resolved.dexList.length && resolved.exchangeList.includes("hyperliquid")) {
602
+ const hlEx = exchanges.find(e => e.name === "hyperliquid");
603
+ const hlAddr = hlEx ? hlEx.adapter.address ?? "" : "";
604
+ if (hlAddr) {
605
+ const detected = await autoDetectHip3Dexes(hlAddr, getHLAdapterForDex, false);
606
+ exchanges.push(...detected);
607
+ }
608
+ }
609
+ }
610
+ if (!exchanges.length) {
611
+ console.log(JSON.stringify(jsonError("NO_EXCHANGES", "No exchange adapters could be initialized. Check your keys.")));
612
+ process.exit(1);
613
+ }
614
+ const dashboard = await startDashboard(exchanges, { port: resolved.servePort, pollInterval: resolved.pollIntervalMs });
615
+ const data = {
616
+ version: "",
617
+ scope: resolved.scope,
618
+ mode: "serve",
619
+ exchanges: [],
620
+ totals: { equity: 0, spotValueUsd: 0, accountValueUsd: 0, marginUsed: 0, marginPct: 0, funding24h: 0 },
621
+ risk: { level: "LOW", marginPct: 0 },
622
+ serve: {
623
+ url: `http://localhost:${dashboard.port}`,
624
+ port: dashboard.port,
625
+ exchanges: exchanges.map(e => e.name),
626
+ },
627
+ };
628
+ printJson(jsonOk(data));
629
+ await new Promise(() => { });
630
+ return;
631
+ }
632
+ console.log(chalk.cyan.bold("\n perp-cli Live Dashboard\n"));
633
+ console.log(chalk.gray(` Initializing exchanges: ${resolved.exchangeList.join(", ")}${resolved.dexList.length ? ` + HIP-3: ${resolved.dexList.join(", ")}` : ""}...\n`));
634
+ const exchanges = [];
635
+ for (const name of resolved.exchangeList) {
636
+ try {
637
+ const adapter = await getAdapterForExchange(name);
638
+ exchanges.push({ name, adapter });
639
+ console.log(chalk.green(` ✓ ${name}`));
640
+ }
641
+ catch (err) {
642
+ const msg = err instanceof Error ? err.message : String(err);
643
+ console.log(chalk.yellow(` ✗ ${name}: ${msg.slice(0, 80)}`));
644
+ }
645
+ }
646
+ if (getHLAdapterForDex) {
647
+ for (const dex of resolved.dexList) {
648
+ try {
649
+ const adapter = await getHLAdapterForDex(dex);
650
+ exchanges.push({ name: `hl:${dex}`, adapter });
651
+ console.log(chalk.green(` ✓ hl:${dex} (HIP-3)`));
652
+ }
653
+ catch (err) {
654
+ const msg = err instanceof Error ? err.message : String(err);
655
+ console.log(chalk.yellow(` ✗ hl:${dex}: ${msg.slice(0, 80)}`));
656
+ }
657
+ }
658
+ if (resolved.autoDex && !resolved.dexList.length && resolved.exchangeList.includes("hyperliquid")) {
659
+ const hlEx = exchanges.find(e => e.name === "hyperliquid");
660
+ const hlAddr = hlEx ? hlEx.adapter.address ?? "" : "";
661
+ if (hlAddr) {
662
+ console.log(chalk.gray(" Scanning HIP-3 dexes for active positions..."));
663
+ const detected = await autoDetectHip3Dexes(hlAddr, getHLAdapterForDex, true);
664
+ exchanges.push(...detected);
665
+ if (!detected.length)
666
+ console.log(chalk.gray(" No active HIP-3 dex positions found"));
667
+ }
668
+ }
669
+ }
670
+ if (!exchanges.length) {
671
+ console.error(chalk.red("\n No exchanges available. Check your private keys in .env\n"));
672
+ process.exit(1);
673
+ }
674
+ console.log(chalk.gray(`\n Starting server on port ${resolved.servePort}...`));
675
+ const dashboard = await startDashboard(exchanges, { port: resolved.servePort, pollInterval: resolved.pollIntervalMs });
676
+ console.log(chalk.cyan.bold("\n Dashboard running at: ") + chalk.white.bold(`http://localhost:${dashboard.port}`));
677
+ console.log(chalk.gray(` Monitoring: ${exchanges.map(e => e.name).join(", ")}`));
678
+ console.log(chalk.gray(` Poll interval: ${resolved.pollIntervalMs}ms`));
679
+ console.log(chalk.gray(" Press Ctrl+C to stop\n"));
680
+ // Open browser automatically (best-effort)
681
+ try {
682
+ const { exec } = await import("child_process");
683
+ const url = `http://localhost:${dashboard.port}`;
684
+ if (process.platform === "darwin")
685
+ exec(`open ${url}`);
686
+ else if (process.platform === "linux")
687
+ exec(`xdg-open ${url}`);
688
+ }
689
+ catch { /* user can open manually */ }
690
+ const ac = new AbortController();
691
+ process.on("SIGINT", () => {
692
+ console.log(chalk.gray("\n Shutting down dashboard..."));
693
+ dashboard.close();
694
+ ac.abort();
695
+ process.exit(0);
696
+ });
697
+ process.on("SIGTERM", () => {
698
+ dashboard.close();
699
+ ac.abort();
700
+ process.exit(0);
701
+ });
702
+ await new Promise(() => { });
703
+ }
704
+ // ── Registration ───────────────────────────────────────────────────────────
705
+ export function registerPortfolioCommand(program, getAdapterForExchange, isJson, version, getHLAdapterForDex) {
97
706
  program
98
707
  .command("portfolio")
99
- .description("Unified cross-exchange portfolio: balances, positions, risk")
100
- .option("--exchange <exchanges>", "Comma-separated exchanges to include (default: all)")
101
- .action(async (opts) => {
102
- await withJsonErrors(isJson(), async () => {
103
- const exchanges = opts.exchange
104
- ? opts.exchange.split(",").map(e => e.trim())
105
- : [...EXCHANGES];
106
- const snapshots = await Promise.all(exchanges.map(ex => fetchExchangeSnapshot(ex, getAdapterForExchange)));
107
- const summary = buildSummary(snapshots);
108
- if (isJson()) {
109
- return printJson(jsonOk(summary));
110
- }
111
- // ── Header ──
112
- console.log(chalk.cyan.bold("\n Cross-Exchange Portfolio\n"));
113
- // ── Balance Summary ──
114
- console.log(chalk.white.bold(" Balances"));
115
- const balRows = snapshots.map(s => {
116
- if (!s.connected) {
117
- return [chalk.white(s.exchange), chalk.red("disconnected"), "-", "-", "-", chalk.gray(s.error ?? "")];
708
+ .description("Unified account view: balances, positions, risk (replaces account balance / status / dashboard)")
709
+ .option("--exchanges <list>", "Comma-separated exchanges to include (default: all). Plural to avoid collision with global -e/--exchange.")
710
+ .option("--arb", "Include top arb opportunities (replaces 'perp status')")
711
+ .option("--health", "Connectivity-only mode (replaces 'perp status --health')")
712
+ .option("--watch [interval]", "Poll mode: clear screen + redraw each tick (ms; default 5000)")
713
+ .option("--serve [port]", "Serve web dashboard (replaces 'perp dashboard')")
714
+ .option("--interval <ms>", "Poll interval for --watch / --serve (default 5000)")
715
+ .option("--dex <list>", "Comma-separated HIP-3 dex names (only with --serve)")
716
+ .option("--no-auto-dex", "Disable HIP-3 dex auto-detection")
717
+ .option("--stdin", "Read JSON args from stdin (overrides flag args)")
718
+ .action(async (flags) => {
719
+ const json = isJson();
720
+ let stdinOpts = {};
721
+ if (flags.stdin) {
722
+ try {
723
+ stdinOpts = await readStdinJson();
724
+ }
725
+ catch (err) {
726
+ const msg = err instanceof Error ? err.message : String(err);
727
+ if (json) {
728
+ console.log(JSON.stringify(jsonError("INVALID_PARAMS", `Invalid stdin JSON: ${msg}`)));
118
729
  }
119
- const b = s.balance;
120
- return [
121
- chalk.white.bold(s.exchange),
122
- `$${formatUsd(b.equity)}`,
123
- `$${formatUsd(b.available)}`,
124
- `$${formatUsd(b.marginUsed)}`,
125
- formatPnl(b.unrealizedPnl),
126
- chalk.green("connected"),
127
- ];
128
- });
129
- // Totals row
130
- balRows.push([
131
- chalk.cyan.bold("TOTAL"),
132
- chalk.cyan.bold(`$${formatUsd(summary.totalEquity)}`),
133
- chalk.cyan.bold(`$${formatUsd(summary.totalAvailable)}`),
134
- chalk.cyan.bold(`$${formatUsd(summary.totalMarginUsed)}`),
135
- formatPnl(summary.totalUnrealizedPnl),
136
- "",
137
- ]);
138
- console.log(makeTable(["Exchange", "Equity", "Available", "Margin Used", "uPnL", "Status"], balRows));
139
- // ── Positions ──
140
- if (summary.positions.length > 0) {
141
- console.log(chalk.white.bold("\n Open Positions"));
142
- const posRows = summary.positions.map(p => {
143
- const sideColor = p.side === "long" ? chalk.green : chalk.red;
144
- const notional = Math.abs(Number(p.size) * Number(p.markPrice));
145
- return [
146
- chalk.white.bold(p.symbol),
147
- chalk.gray(p.exchange),
148
- sideColor(p.side.toUpperCase()),
149
- p.size,
150
- `$${formatUsd(p.entryPrice)}`,
151
- `$${formatUsd(p.markPrice)}`,
152
- formatPnl(p.unrealizedPnl),
153
- `$${formatUsd(notional)}`,
154
- `${p.leverage}x`,
155
- ];
156
- });
157
- console.log(makeTable(["Symbol", "Exchange", "Side", "Size", "Entry", "Mark", "uPnL", "Notional", "Lev"], posRows));
730
+ else {
731
+ console.error(chalk.red(`Error: invalid stdin JSON — ${msg}`));
732
+ }
733
+ process.exit(1);
734
+ }
735
+ }
736
+ let resolved;
737
+ try {
738
+ resolved = resolveOptions(program, flags, stdinOpts);
739
+ }
740
+ catch (err) {
741
+ const msg = err instanceof Error ? err.message : String(err);
742
+ if (json) {
743
+ console.log(JSON.stringify(jsonError("INVALID_PARAMS", msg)));
158
744
  }
159
745
  else {
160
- console.log(chalk.gray("\n No open positions.\n"));
161
- }
162
- // ── Risk Metrics ──
163
- console.log(chalk.white.bold("\n Risk Metrics"));
164
- const levelColor = {
165
- low: chalk.green,
166
- medium: chalk.yellow,
167
- high: chalk.red,
168
- critical: chalk.bgRed.white,
169
- }[summary.risk.level];
170
- console.log(` Risk Level: ${levelColor(summary.risk.level.toUpperCase())}`);
171
- console.log(` Can Trade: ${summary.risk.canTrade ? chalk.green("YES") : chalk.red("NO")}`);
172
- const mu = summary.riskMetrics.marginUtilization;
173
- const muColor = mu < 30 ? chalk.green : mu < 60 ? chalk.yellow : chalk.red;
174
- console.log(` Margin Utilization: ${muColor(`${mu.toFixed(1)}%`)}`);
175
- if (summary.riskMetrics.largestPosition) {
176
- const lp = summary.riskMetrics.largestPosition;
177
- console.log(` Largest Position: ${lp.symbol} on ${lp.exchange} ($${formatUsd(lp.notional)})`);
178
- }
179
- if (summary.riskMetrics.exchangeConcentration.length > 0) {
180
- console.log(` Exchange Allocation:`);
181
- for (const ec of summary.riskMetrics.exchangeConcentration) {
182
- const bar = "\u2588".repeat(Math.round(ec.pct / 5)) + "\u2591".repeat(20 - Math.round(ec.pct / 5));
183
- console.log(` ${ec.exchange.padEnd(12)} ${bar} ${ec.pct.toFixed(1)}%`);
184
- }
746
+ console.error(chalk.red(`Error: ${msg}`));
185
747
  }
186
- if (summary.risk.violations.length > 0) {
187
- console.log(chalk.red.bold("\n Risk Violations"));
188
- for (const v of summary.risk.violations) {
189
- const sevColor = { low: chalk.green, medium: chalk.yellow, high: chalk.red, critical: chalk.bgRed.white }[v.severity];
190
- console.log(` ${sevColor(v.severity.toUpperCase().padEnd(8))} ${v.message}`);
191
- }
748
+ process.exit(1);
749
+ }
750
+ // Mode dispatch (mutually exclusive — checked in priority order)
751
+ if (resolved.serve) {
752
+ return runServe(resolved, getAdapterForExchange, json, getHLAdapterForDex);
753
+ }
754
+ if (resolved.watch) {
755
+ return runWatch(resolved, version, getAdapterForExchange, json);
756
+ }
757
+ await withJsonErrors(json, async () => {
758
+ if (resolved.health) {
759
+ const health = await runHealth(resolved.exchangeList);
760
+ const data = {
761
+ version,
762
+ scope: resolved.scope,
763
+ mode: "health",
764
+ exchanges: [],
765
+ totals: { equity: 0, spotValueUsd: 0, accountValueUsd: 0, marginUsed: 0, marginPct: 0, funding24h: 0 },
766
+ risk: { level: "LOW", marginPct: 0 },
767
+ health,
768
+ };
769
+ if (json)
770
+ return printJson(jsonOk(data));
771
+ renderHealthText(health);
772
+ return;
192
773
  }
193
- console.log();
774
+ const data = await buildSnapshotEnvelope(resolved, version, getAdapterForExchange, "snapshot");
775
+ if (json)
776
+ return printJson(jsonOk(data));
777
+ renderSnapshotText(data);
194
778
  });
195
779
  });
196
780
  }