perp-cli 0.11.0 → 0.12.1
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.
- package/README.md +30 -25
- package/dist/_stderr-filter.d.ts +0 -0
- package/dist/_stderr-filter.js +30 -0
- package/dist/agent-wallet/expiry.d.ts +15 -0
- package/dist/agent-wallet/expiry.js +26 -0
- package/dist/agent-wallet/lighter-keystore.d.ts +34 -0
- package/dist/agent-wallet/lighter-keystore.js +232 -0
- package/dist/agent-wallet/passphrase.d.ts +13 -0
- package/dist/agent-wallet/passphrase.js +54 -0
- package/dist/agent-wallet/signing-strategy.d.ts +67 -0
- package/dist/agent-wallet/signing-strategy.js +51 -0
- package/dist/agent-wallet/store.d.ts +37 -0
- package/dist/agent-wallet/store.js +172 -0
- package/dist/api/public/_http.d.ts +15 -0
- package/dist/api/public/_http.js +24 -0
- package/dist/api/public/hyperliquid.js +36 -24
- package/dist/api/public/lighter.d.ts +7 -1
- package/dist/api/public/lighter.js +85 -37
- package/dist/api/public/pacifica.js +47 -18
- package/dist/bridge-engine.js +102 -49
- package/dist/cli-spec.js +1 -2
- package/dist/commands/account.js +2 -500
- package/dist/commands/agent.d.ts +8 -2
- package/dist/commands/agent.js +1903 -366
- package/dist/commands/alerts.js +3 -1
- package/dist/commands/arb/index.js +37 -7
- package/dist/commands/arb-auto.js +44 -11
- package/dist/commands/arb.js +83 -34
- package/dist/commands/bot.d.ts +1 -1
- package/dist/commands/bot.js +18 -14
- package/dist/commands/bridge.d.ts +1 -1
- package/dist/commands/bridge.js +4 -4
- package/dist/commands/funds.d.ts +1 -1
- package/dist/commands/funds.js +18 -68
- package/dist/commands/jobs.d.ts +1 -1
- package/dist/commands/jobs.js +2 -2
- package/dist/commands/manage.d.ts +1 -1
- package/dist/commands/manage.js +12 -84
- package/dist/commands/plan.d.ts +1 -1
- package/dist/commands/plan.js +2 -2
- package/dist/commands/portfolio.d.ts +19 -3
- package/dist/commands/portfolio.js +747 -163
- package/dist/commands/rebalance.d.ts +1 -1
- package/dist/commands/rebalance.js +18 -12
- package/dist/commands/risk.js +2 -6
- package/dist/commands/settings.js +30 -53
- package/dist/commands/trade.js +16 -15
- package/dist/commands/wallet.d.ts +2 -1
- package/dist/commands/wallet.js +756 -28
- package/dist/errors.d.ts +64 -0
- package/dist/errors.js +82 -2
- package/dist/event-stream.js +12 -1
- package/dist/exchanges/aster-typed-data.d.ts +156 -0
- package/dist/exchanges/aster-typed-data.js +189 -0
- package/dist/exchanges/aster.d.ts +60 -11
- package/dist/exchanges/aster.js +278 -136
- package/dist/exchanges/hyperliquid.d.ts +14 -0
- package/dist/exchanges/hyperliquid.js +84 -34
- package/dist/exchanges/lighter-spot.js +14 -3
- package/dist/exchanges/lighter.d.ts +66 -0
- package/dist/exchanges/lighter.js +199 -27
- package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
- package/dist/exchanges/pacifica-typed-data.js +74 -0
- package/dist/exchanges/pacifica.d.ts +29 -1
- package/dist/exchanges/pacifica.js +94 -6
- package/dist/funding/rates.js +59 -21
- package/dist/guardrail/perp-guardrail.js +9 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +268 -360
- package/dist/jobs.js +3 -1
- package/dist/mcp-server.js +85 -83
- package/dist/position-history.js +47 -4
- package/dist/settings.d.ts +67 -3
- package/dist/settings.js +1 -2
- package/dist/signer/ows-evm.d.ts +14 -0
- package/dist/signer/ows-evm.js +28 -9
- package/dist/smart-order.d.ts +8 -2
- package/dist/smart-order.js +46 -11
- package/dist/strategies/funding-arb.js +29 -8
- package/dist/trade-validator.js +30 -5
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +19 -0
- package/package.json +2 -2
- package/skills/perp-cli/SKILL.md +23 -14
- package/skills/perp-cli/references/agent-operations.md +6 -4
- package/skills/perp-cli/references/commands.md +55 -34
- package/skills/perp-cli/scripts/preflight.sh +4 -4
- package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
- package/dist/__tests__/alert-logic.test.d.ts +0 -1
- package/dist/__tests__/alert-logic.test.js +0 -107
- package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
- package/dist/__tests__/arb-auto-3dex.test.js +0 -397
- package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
- package/dist/__tests__/arb-history-stats.test.js +0 -176
- package/dist/__tests__/arb-logic.test.d.ts +0 -1
- package/dist/__tests__/arb-logic.test.js +0 -84
- package/dist/__tests__/arb-manage.test.d.ts +0 -1
- package/dist/__tests__/arb-manage.test.js +0 -253
- package/dist/__tests__/arb-new-features.test.d.ts +0 -1
- package/dist/__tests__/arb-new-features.test.js +0 -457
- package/dist/__tests__/arb-sizing.test.d.ts +0 -1
- package/dist/__tests__/arb-sizing.test.js +0 -48
- package/dist/__tests__/arb-state.test.d.ts +0 -1
- package/dist/__tests__/arb-state.test.js +0 -284
- package/dist/__tests__/arb-userflow.test.d.ts +0 -1
- package/dist/__tests__/arb-userflow.test.js +0 -945
- package/dist/__tests__/arb-utils.test.d.ts +0 -1
- package/dist/__tests__/arb-utils.test.js +0 -264
- package/dist/__tests__/bot-conditions.test.d.ts +0 -1
- package/dist/__tests__/bot-conditions.test.js +0 -341
- package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
- package/dist/__tests__/client-id-tracker.test.js +0 -137
- package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
- package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
- package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
- package/dist/__tests__/commands/order-intent.test.js +0 -600
- package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
- package/dist/__tests__/commands/trade-commands.test.js +0 -821
- package/dist/__tests__/config.test.d.ts +0 -1
- package/dist/__tests__/config.test.js +0 -86
- package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
- package/dist/__tests__/cross-chain-margin.test.js +0 -287
- package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
- package/dist/__tests__/dex-asset-map.test.js +0 -191
- package/dist/__tests__/errors.test.d.ts +0 -1
- package/dist/__tests__/errors.test.js +0 -110
- package/dist/__tests__/event-stream.test.d.ts +0 -1
- package/dist/__tests__/event-stream.test.js +0 -276
- package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
- package/dist/__tests__/exchanges/interface.test.js +0 -132
- package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
- package/dist/__tests__/exchanges/mock-adapter.js +0 -137
- package/dist/__tests__/execution-log.test.d.ts +0 -1
- package/dist/__tests__/execution-log.test.js +0 -106
- package/dist/__tests__/funding-calc.test.d.ts +0 -1
- package/dist/__tests__/funding-calc.test.js +0 -71
- package/dist/__tests__/funding-history.test.d.ts +0 -1
- package/dist/__tests__/funding-history.test.js +0 -343
- package/dist/__tests__/funding-rates.test.d.ts +0 -1
- package/dist/__tests__/funding-rates.test.js +0 -342
- package/dist/__tests__/funding.test.d.ts +0 -1
- package/dist/__tests__/funding.test.js +0 -173
- package/dist/__tests__/gap-logic.test.d.ts +0 -1
- package/dist/__tests__/gap-logic.test.js +0 -43
- package/dist/__tests__/hip3-dex.test.d.ts +0 -1
- package/dist/__tests__/hip3-dex.test.js +0 -234
- package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
- package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
- package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
- package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
- package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
- package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/bridge.integration.test.js +0 -309
- package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
- package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
- package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
- package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
- package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
- package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
- package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/lighter.integration.test.js +0 -53
- package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
- package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
- package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
- package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
- package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
- package/dist/__tests__/liquidity.test.d.ts +0 -1
- package/dist/__tests__/liquidity.test.js +0 -225
- package/dist/__tests__/plan-executor.test.d.ts +0 -1
- package/dist/__tests__/plan-executor.test.js +0 -314
- package/dist/__tests__/position-history.test.d.ts +0 -1
- package/dist/__tests__/position-history.test.js +0 -367
- package/dist/__tests__/retry.test.d.ts +0 -1
- package/dist/__tests__/retry.test.js +0 -310
- package/dist/__tests__/risk-assessment.test.d.ts +0 -1
- package/dist/__tests__/risk-assessment.test.js +0 -288
- package/dist/__tests__/security-adversarial.test.d.ts +0 -1
- package/dist/__tests__/security-adversarial.test.js +0 -574
- package/dist/__tests__/strategies.test.d.ts +0 -1
- package/dist/__tests__/strategies.test.js +0 -539
- package/dist/__tests__/trade-execution.test.d.ts +0 -1
- package/dist/__tests__/trade-execution.test.js +0 -129
- package/dist/__tests__/trade-validator.test.d.ts +0 -1
- package/dist/__tests__/trade-validator.test.js +0 -655
- package/dist/__tests__/utils.test.d.ts +0 -1
- package/dist/__tests__/utils.test.js +0 -76
- package/dist/arb/reconcile.d.ts +0 -28
- package/dist/arb/reconcile.js +0 -162
- package/dist/commands/alert.d.ts +0 -2
- package/dist/commands/alert.js +0 -421
- package/dist/commands/analytics.d.ts +0 -3
- package/dist/commands/analytics.js +0 -311
- package/dist/commands/arb-manage.d.ts +0 -5
- package/dist/commands/arb-manage.js +0 -5
- package/dist/commands/dashboard.d.ts +0 -3
- package/dist/commands/dashboard.js +0 -176
- package/dist/commands/deposit.d.ts +0 -4
- package/dist/commands/deposit.js +0 -573
- package/dist/commands/dex.d.ts +0 -3
- package/dist/commands/dex.js +0 -120
- package/dist/commands/env.d.ts +0 -2
- package/dist/commands/env.js +0 -136
- package/dist/commands/funding.d.ts +0 -3
- package/dist/commands/funding.js +0 -554
- package/dist/commands/gap.d.ts +0 -2
- package/dist/commands/gap.js +0 -305
- package/dist/commands/health.d.ts +0 -2
- package/dist/commands/health.js +0 -67
- package/dist/commands/multileg.d.ts +0 -16
- package/dist/commands/multileg.js +0 -174
- package/dist/commands/ows.d.ts +0 -2
- package/dist/commands/ows.js +0 -887
- package/dist/commands/pnl.d.ts +0 -12
- package/dist/commands/pnl.js +0 -243
- package/dist/commands/run.d.ts +0 -3
- package/dist/commands/run.js +0 -202
- package/dist/commands/stream.d.ts +0 -5
- package/dist/commands/stream.js +0 -200
- package/dist/commands/withdraw.d.ts +0 -3
- package/dist/commands/withdraw.js +0 -187
- package/dist/exchanges/edgex.d.ts +0 -62
- package/dist/exchanges/edgex.js +0 -552
- package/dist/exchanges/nado.d.ts +0 -52
- package/dist/exchanges/nado.js +0 -223
- package/dist/funding/index.d.ts +0 -3
- package/dist/funding/index.js +0 -3
- package/dist/retry.d.ts +0 -74
- package/dist/retry.js +0 -129
- package/dist/ws/market-feed.d.ts +0 -74
- package/dist/ws/market-feed.js +0 -227
|
@@ -1,196 +1,780 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
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(
|
|
8
|
-
const [
|
|
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
|
-
|
|
216
|
+
name: exName,
|
|
15
217
|
connected: true,
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
243
|
+
name: exName,
|
|
24
244
|
connected: false,
|
|
25
|
-
|
|
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
|
|
33
|
-
let
|
|
34
|
-
let
|
|
35
|
-
let
|
|
36
|
-
let
|
|
37
|
-
let
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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("[2J[H");
|
|
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
|
|
100
|
-
.option("--
|
|
101
|
-
.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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.
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
}
|