perp-cli 0.14.1 → 0.14.2
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/dist/arb/sizing.js +29 -7
- package/dist/bot/conditions.d.ts +5 -1
- package/dist/bot/conditions.js +16 -11
- package/dist/bot/engine.d.ts +12 -0
- package/dist/bot/engine.js +55 -44
- package/dist/bot/strategies/claude-agent.js +2 -2
- package/dist/bot/strategies/funding-arb-strategy.js +16 -20
- package/dist/bot/strategies/funding-arb-utils.d.ts +19 -0
- package/dist/bot/strategies/funding-arb-utils.js +44 -0
- package/dist/bot/strategies/funding-auto.js +61 -37
- package/dist/bot/strategies/grid-strategy.d.ts +2 -0
- package/dist/bot/strategies/grid-strategy.js +21 -2
- package/dist/bot/strategies/spot-perp-arb-strategy.js +28 -20
- package/dist/bot/tui/BotDashboard.d.ts +13 -2
- package/dist/bot/tui/BotDashboard.js +8 -3
- package/dist/bridge-engine.d.ts +2 -0
- package/dist/bridge-engine.js +68 -31
- package/dist/commands/account.js +2 -2
- package/dist/commands/arb/index.d.ts +20 -0
- package/dist/commands/arb/index.js +121 -36
- package/dist/commands/arb-auto.d.ts +36 -0
- package/dist/commands/arb-auto.js +202 -76
- package/dist/commands/arb.js +51 -35
- package/dist/commands/bot.d.ts +6 -0
- package/dist/commands/bot.js +55 -45
- package/dist/commands/funds-onchain.js +6 -5
- package/dist/commands/funds.js +18 -5
- package/dist/commands/history.js +135 -53
- package/dist/commands/portfolio.d.ts +5 -0
- package/dist/commands/portfolio.js +58 -20
- package/dist/commands/rebalance.js +46 -10
- package/dist/commands/settings.js +2 -1
- package/dist/commands/trade.js +8 -4
- package/dist/commands/wallet.js +1 -1
- package/dist/cross-chain-margin.d.ts +29 -2
- package/dist/cross-chain-margin.js +30 -4
- package/dist/dashboard/server.d.ts +45 -8
- package/dist/dashboard/server.js +113 -42
- package/dist/dashboard/ui.js +46 -27
- package/dist/dashboard/ws-feeds.d.ts +25 -3
- package/dist/dashboard/ws-feeds.js +219 -82
- package/dist/dex-asset-map.d.ts +23 -4
- package/dist/dex-asset-map.js +39 -13
- package/dist/exchanges/arcus/adapter.js +33 -6
- package/dist/exchanges/arcus/public.d.ts +6 -0
- package/dist/exchanges/arcus/public.js +12 -4
- package/dist/exchanges/aster.d.ts +19 -7
- package/dist/exchanges/aster.js +79 -46
- package/dist/exchanges/hyperliquid.d.ts +30 -0
- package/dist/exchanges/hyperliquid.js +159 -34
- package/dist/exchanges/lighter.d.ts +1 -1
- package/dist/exchanges/lighter.js +11 -9
- package/dist/exchanges/ondo/funds.d.ts +0 -1
- package/dist/exchanges/pacifica.d.ts +7 -0
- package/dist/exchanges/pacifica.js +52 -26
- package/dist/rebalance.d.ts +20 -1
- package/dist/rebalance.js +31 -5
- package/dist/strategies/funding-arb.js +38 -7
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +2 -2
- package/skills/perp-cli/references/commands.md +5 -3
|
@@ -176,7 +176,7 @@ async function fetchExchangeEntry(exName, getAdapter) {
|
|
|
176
176
|
adapter.getBalance(),
|
|
177
177
|
adapter.getPositions(),
|
|
178
178
|
adapter.getOpenOrders(),
|
|
179
|
-
adapter.getFundingPayments(200).
|
|
179
|
+
adapter.getFundingPayments(200).then((rows) => ({ rows, error: null }), (e) => ({ rows: null, error: e instanceof Error ? e.message : String(e) })),
|
|
180
180
|
]);
|
|
181
181
|
// Spot balances for every venue with def.spot (§12). Failures are surfaced on
|
|
182
182
|
// stderr (the perp snapshot is still reported); unpriced tokens are excluded + warned.
|
|
@@ -200,8 +200,11 @@ async function fetchExchangeEntry(exName, getAdapter) {
|
|
|
200
200
|
warn(`${def.displayName} spot balances unavailable: ${e instanceof Error ? e.message : String(e)}`);
|
|
201
201
|
}
|
|
202
202
|
const cutoff24h = Date.now() - 24 * 60 * 60 * 1000;
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
if (fundingPayments.error !== null)
|
|
204
|
+
warn(`${def.displayName} funding history unavailable (funding24h null): ${fundingPayments.error}`);
|
|
205
|
+
const funding24h = fundingPayments.rows === null
|
|
206
|
+
? null
|
|
207
|
+
: fundingPayments.rows.filter(f => f.time >= cutoff24h).reduce((sum, f) => sum + Number(f.payment), 0);
|
|
205
208
|
// Shared balances (Pacifica USDC, HL unified USDC) are already in perp equity (§12.4 #8).
|
|
206
209
|
const nonUsdcSpotValue = spot.filter(b => !b.shared).reduce((s, b) => s + b.valueUsd, 0);
|
|
207
210
|
return {
|
|
@@ -227,6 +230,7 @@ async function fetchExchangeEntry(exName, getAdapter) {
|
|
|
227
230
|
})),
|
|
228
231
|
openOrders: orders.length,
|
|
229
232
|
funding24h,
|
|
233
|
+
fundingError: fundingPayments.error,
|
|
230
234
|
totalAccountValueUsd: Number(bal.equity) + nonUsdcSpotValue,
|
|
231
235
|
error: null,
|
|
232
236
|
};
|
|
@@ -240,7 +244,9 @@ async function fetchExchangeEntry(exName, getAdapter) {
|
|
|
240
244
|
spot: [],
|
|
241
245
|
positions: [],
|
|
242
246
|
openOrders: 0,
|
|
243
|
-
funding24h:
|
|
247
|
+
funding24h: null,
|
|
248
|
+
// The whole venue was unreadable: funding is unknown for the same reason.
|
|
249
|
+
fundingError: err instanceof Error ? err.message : String(err),
|
|
244
250
|
totalAccountValueUsd: 0,
|
|
245
251
|
error: err instanceof Error ? err.message : String(err),
|
|
246
252
|
};
|
|
@@ -251,13 +257,17 @@ function buildTotals(entries) {
|
|
|
251
257
|
let spotValueUsd = 0;
|
|
252
258
|
let marginUsed = 0;
|
|
253
259
|
let funding24h = 0;
|
|
260
|
+
let fundingIncomplete = false;
|
|
254
261
|
let accountValueUsd = 0;
|
|
255
262
|
for (const e of entries) {
|
|
256
263
|
if (e.perp) {
|
|
257
264
|
equity += Number(e.perp.equity);
|
|
258
265
|
marginUsed += Number(e.perp.marginUsed);
|
|
259
266
|
}
|
|
260
|
-
|
|
267
|
+
if (e.funding24h === null)
|
|
268
|
+
fundingIncomplete = true;
|
|
269
|
+
else
|
|
270
|
+
funding24h += e.funding24h;
|
|
261
271
|
// Non-USDC spot value (for unified accounts USDC is already in equity)
|
|
262
272
|
const exSpot = e.isUnified
|
|
263
273
|
? e.spot.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
|
|
@@ -272,7 +282,7 @@ function buildTotals(entries) {
|
|
|
272
282
|
const marginPct = denom > 0 ? (marginUsed / denom) * 100 : 0;
|
|
273
283
|
const level = marginPct < 30 ? "LOW" : marginPct < 60 ? "MEDIUM" : "HIGH";
|
|
274
284
|
return {
|
|
275
|
-
totals: { equity, spotValueUsd, accountValueUsd, marginUsed, marginPct, funding24h },
|
|
285
|
+
totals: { equity, spotValueUsd, accountValueUsd, marginUsed, marginPct, funding24h, fundingIncomplete },
|
|
276
286
|
risk: { level, marginPct },
|
|
277
287
|
};
|
|
278
288
|
}
|
|
@@ -537,7 +547,11 @@ async function runWatch(resolved, version, getAdapterForExchange, isJsonMode) {
|
|
|
537
547
|
}
|
|
538
548
|
// ── Serve mode (delegates to dashboard infra) ──────────────────────────────
|
|
539
549
|
const KNOWN_HIP3_DEXES = ["xyz", "flx", "hyna", "km", "cash", "vntl"];
|
|
540
|
-
|
|
550
|
+
/**
|
|
551
|
+
* Open-position count on a HIP-3 dex; null = the read failed (unknown — NOT "no positions").
|
|
552
|
+
* The failure is warned on stderr; callers must treat null as "may hold positions".
|
|
553
|
+
*/
|
|
554
|
+
export async function checkDexPositions(address, dex) {
|
|
541
555
|
try {
|
|
542
556
|
// Selected network (review L7): follows -n via setSharedApiNetwork (live binding).
|
|
543
557
|
const res = await fetch(HYPERLIQUID_API_URL, {
|
|
@@ -545,15 +559,22 @@ async function checkDexPositions(address, dex) {
|
|
|
545
559
|
headers: { "Content-Type": "application/json" },
|
|
546
560
|
body: JSON.stringify({ type: "clearinghouseState", user: address, dex }),
|
|
547
561
|
});
|
|
562
|
+
if (!res.ok)
|
|
563
|
+
throw new Error(`HTTP ${res.status}`);
|
|
548
564
|
const state = await res.json();
|
|
549
|
-
|
|
550
|
-
|
|
565
|
+
if (!state || !Array.isArray(state.assetPositions))
|
|
566
|
+
throw new Error("clearinghouseState without assetPositions");
|
|
567
|
+
return state.assetPositions.filter(p => {
|
|
551
568
|
const pos = (p.position ?? p);
|
|
552
|
-
|
|
569
|
+
const szi = Number(pos.szi);
|
|
570
|
+
if (!Number.isFinite(szi))
|
|
571
|
+
throw new Error(`position without a numeric szi (${String(pos.coin)})`);
|
|
572
|
+
return szi !== 0;
|
|
553
573
|
}).length;
|
|
554
574
|
}
|
|
555
|
-
catch {
|
|
556
|
-
|
|
575
|
+
catch (e) {
|
|
576
|
+
process.stderr.write(`[warn] hl:${dex} positions unknown (HIP-3 dex read failed; included in the dashboard): ${e instanceof Error ? e.message : String(e)}\n`);
|
|
577
|
+
return null;
|
|
557
578
|
}
|
|
558
579
|
}
|
|
559
580
|
async function autoDetectHip3Dexes(address, getHLAdapterForDex, verbose) {
|
|
@@ -561,16 +582,22 @@ async function autoDetectHip3Dexes(address, getHLAdapterForDex, verbose) {
|
|
|
561
582
|
dex,
|
|
562
583
|
count: await checkDexPositions(address, dex),
|
|
563
584
|
})));
|
|
564
|
-
|
|
585
|
+
// Unknown (null) counts are included: a failed read must not hide a dex that may hold positions.
|
|
586
|
+
const activeDexes = checks.filter(c => c.count === null || c.count > 0);
|
|
565
587
|
const results = [];
|
|
566
588
|
for (const { dex, count } of activeDexes) {
|
|
567
589
|
try {
|
|
568
590
|
const adapter = await getHLAdapterForDex(dex);
|
|
569
591
|
results.push({ name: `hl:${dex}`, adapter });
|
|
570
|
-
if (verbose)
|
|
571
|
-
console.log(
|
|
592
|
+
if (verbose) {
|
|
593
|
+
console.log(count === null
|
|
594
|
+
? chalk.yellow(` ⚠ hl:${dex} (HIP-3, positions unknown — read failed)`)
|
|
595
|
+
: chalk.green(` ✓ hl:${dex} (HIP-3, ${count} position${count > 1 ? "s" : ""})`));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
catch (e) {
|
|
599
|
+
process.stderr.write(`[warn] hl:${dex} left out of the dashboard (adapter unavailable${count === null ? ", positions unknown" : `, ${count} position(s)`}): ${e instanceof Error ? e.message : String(e)}\n`);
|
|
572
600
|
}
|
|
573
|
-
catch { /* skip */ }
|
|
574
601
|
}
|
|
575
602
|
return results;
|
|
576
603
|
}
|
|
@@ -578,12 +605,20 @@ async function runServe(resolved, getAdapterForExchange, isJsonMode, getHLAdapte
|
|
|
578
605
|
const { startDashboard } = await import("../dashboard/index.js");
|
|
579
606
|
if (isJsonMode) {
|
|
580
607
|
const exchanges = [];
|
|
608
|
+
// A venue whose adapter failed is not monitored — reported in serve.errors + stderr, not dropped silently.
|
|
609
|
+
const errors = {};
|
|
610
|
+
const noteError = (name, e) => {
|
|
611
|
+
errors[name] = e instanceof Error ? e.message : String(e);
|
|
612
|
+
process.stderr.write(`[warn] ${name} not monitored (adapter unavailable): ${errors[name]}\n`);
|
|
613
|
+
};
|
|
581
614
|
for (const name of resolved.exchangeList) {
|
|
582
615
|
try {
|
|
583
616
|
const adapter = await getAdapterForExchange(name);
|
|
584
617
|
exchanges.push({ name, adapter });
|
|
585
618
|
}
|
|
586
|
-
catch {
|
|
619
|
+
catch (e) {
|
|
620
|
+
noteError(name, e);
|
|
621
|
+
}
|
|
587
622
|
}
|
|
588
623
|
if (getHLAdapterForDex) {
|
|
589
624
|
for (const dex of resolved.dexList) {
|
|
@@ -591,7 +626,9 @@ async function runServe(resolved, getAdapterForExchange, isJsonMode, getHLAdapte
|
|
|
591
626
|
const adapter = await getHLAdapterForDex(dex);
|
|
592
627
|
exchanges.push({ name: `hl:${dex}`, adapter });
|
|
593
628
|
}
|
|
594
|
-
catch {
|
|
629
|
+
catch (e) {
|
|
630
|
+
noteError(`hl:${dex}`, e);
|
|
631
|
+
}
|
|
595
632
|
}
|
|
596
633
|
if (resolved.autoDex && !resolved.dexList.length && resolved.exchangeList.includes("hyperliquid")) {
|
|
597
634
|
const hlEx = exchanges.find(e => e.name === "hyperliquid");
|
|
@@ -612,12 +649,13 @@ async function runServe(resolved, getAdapterForExchange, isJsonMode, getHLAdapte
|
|
|
612
649
|
scope: resolved.scope,
|
|
613
650
|
mode: "serve",
|
|
614
651
|
exchanges: [],
|
|
615
|
-
totals: { equity: 0, spotValueUsd: 0, accountValueUsd: 0, marginUsed: 0, marginPct: 0, funding24h: 0 },
|
|
652
|
+
totals: { equity: 0, spotValueUsd: 0, accountValueUsd: 0, marginUsed: 0, marginPct: 0, funding24h: 0, fundingIncomplete: false },
|
|
616
653
|
risk: { level: "LOW", marginPct: 0 },
|
|
617
654
|
serve: {
|
|
618
655
|
url: `http://localhost:${dashboard.port}`,
|
|
619
656
|
port: dashboard.port,
|
|
620
657
|
exchanges: exchanges.map(e => e.name),
|
|
658
|
+
errors,
|
|
621
659
|
},
|
|
622
660
|
};
|
|
623
661
|
printJson(jsonOk(data));
|
|
@@ -757,7 +795,7 @@ export function registerPortfolioCommand(program, getAdapterForExchange, isJson,
|
|
|
757
795
|
scope: resolved.scope,
|
|
758
796
|
mode: "health",
|
|
759
797
|
exchanges: [],
|
|
760
|
-
totals: { equity: 0, spotValueUsd: 0, accountValueUsd: 0, marginUsed: 0, marginPct: 0, funding24h: 0 },
|
|
798
|
+
totals: { equity: 0, spotValueUsd: 0, accountValueUsd: 0, marginUsed: 0, marginPct: 0, funding24h: 0, fundingIncomplete: false },
|
|
761
799
|
risk: { level: "LOW", marginPct: 0 },
|
|
762
800
|
health,
|
|
763
801
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { isDryRun, supportsDryRun } from "../cli-flags.js";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { formatUsd, makeTable, printJson, jsonOk } from "../utils.js";
|
|
4
|
-
import {
|
|
4
|
+
import { PerpError } from "../errors.js";
|
|
5
|
+
import { fetchBalancesWithFailures, computeRebalancePlan, describeBridgeRoute, estimateMoveTime, } from "../rebalance.js";
|
|
5
6
|
import { EXCHANGE_TO_CHAIN, assertBridgeNetwork, executeBestBridge } from "../bridge-engine.js";
|
|
6
7
|
import { cliCredentialOptsFrom } from "../config.js";
|
|
7
8
|
export function registerFundsRebalanceCommands(parent, getAdapterForExchange, isJson, getNetwork) {
|
|
@@ -31,9 +32,14 @@ export function registerFundsRebalanceCommands(parent, getAdapterForExchange, is
|
|
|
31
32
|
}
|
|
32
33
|
if (!isJson())
|
|
33
34
|
console.log(chalk.cyan("\n Fetching balances across exchanges...\n"));
|
|
34
|
-
const snapshots = await
|
|
35
|
+
const { snapshots, failed, notConfigured } = await fetchBalancesWithFailures(adapters);
|
|
36
|
+
// A venue whose balance read failed is unavailable — listed, never shown as $0 or dropped.
|
|
37
|
+
unavailable.push(...failed);
|
|
38
|
+
const partial = unavailable.length > 0;
|
|
35
39
|
if (isJson())
|
|
36
|
-
return printJson(jsonOk({ snapshots, unavailable }));
|
|
40
|
+
return printJson(jsonOk({ snapshots, unavailable, partial, notConfigured }));
|
|
41
|
+
for (const n of notConfigured)
|
|
42
|
+
console.log(chalk.gray(` ${n}: not configured (no credentials) — left out`));
|
|
37
43
|
const totalEquity = snapshots.reduce((s, e) => s + e.equity, 0);
|
|
38
44
|
const totalAvailable = snapshots.reduce((s, e) => s + e.available, 0);
|
|
39
45
|
const rows = snapshots.map((s) => {
|
|
@@ -53,7 +59,8 @@ export function registerFundsRebalanceCommands(parent, getAdapterForExchange, is
|
|
|
53
59
|
console.log(chalk.cyan.bold(" Totals"));
|
|
54
60
|
console.log(` Total Equity: $${formatUsd(totalEquity)}`);
|
|
55
61
|
console.log(` Total Available: $${formatUsd(totalAvailable)}`);
|
|
56
|
-
console.log(` Exchanges: ${snapshots.length}\n`);
|
|
62
|
+
console.log(` Exchanges: ${snapshots.length}${partial ? ` of ${exchangeNames.length} (partial)` : ""}\n`);
|
|
63
|
+
printUnavailable(unavailable);
|
|
57
64
|
});
|
|
58
65
|
// ── rebalance plan ──
|
|
59
66
|
rebalance
|
|
@@ -82,13 +89,22 @@ export function registerFundsRebalanceCommands(parent, getAdapterForExchange, is
|
|
|
82
89
|
}
|
|
83
90
|
if (!isJson())
|
|
84
91
|
console.log(chalk.cyan("\n Computing rebalance plan...\n"));
|
|
85
|
-
const snapshots = await
|
|
92
|
+
const { snapshots, failed, notConfigured } = await fetchBalancesWithFailures(adapters);
|
|
93
|
+
unavailable.push(...failed);
|
|
94
|
+
// partial: the plan covers only the venues that were read — `execute` refuses to run it.
|
|
95
|
+
const partial = unavailable.length > 0;
|
|
86
96
|
const plan = computeRebalancePlan(snapshots, {
|
|
87
97
|
minMove: parseFloat(opts.minMove),
|
|
88
98
|
reserve: parseFloat(opts.reserve),
|
|
89
99
|
});
|
|
90
100
|
if (isJson())
|
|
91
|
-
return printJson(jsonOk({ ...plan, unavailable }));
|
|
101
|
+
return printJson(jsonOk({ ...plan, unavailable, partial, notConfigured }));
|
|
102
|
+
for (const n of notConfigured)
|
|
103
|
+
console.log(chalk.gray(` ${n}: not configured (no credentials) — left out of the plan`));
|
|
104
|
+
if (partial) {
|
|
105
|
+
console.log(chalk.yellow(` ⚠ Partial plan: computed over ${snapshots.length} of ${exchangeNames.length} exchanges.`));
|
|
106
|
+
printUnavailable(unavailable);
|
|
107
|
+
}
|
|
92
108
|
// Show current state
|
|
93
109
|
const rows = plan.snapshots.map((s) => {
|
|
94
110
|
const target = plan.targetPerExchange;
|
|
@@ -134,19 +150,33 @@ export function registerFundsRebalanceCommands(parent, getAdapterForExchange, is
|
|
|
134
150
|
assertBridgeNetwork(getNetwork());
|
|
135
151
|
const exchangeNames = opts.exchanges.split(",").map((e) => e.trim());
|
|
136
152
|
const adapters = new Map();
|
|
153
|
+
const unavailable = [];
|
|
137
154
|
for (const name of exchangeNames) {
|
|
138
155
|
try {
|
|
139
156
|
adapters.set(name, await getAdapterForExchange(name));
|
|
140
157
|
}
|
|
141
|
-
catch {
|
|
142
|
-
|
|
158
|
+
catch (e) {
|
|
159
|
+
unavailable.push({ exchange: name, error: e instanceof Error ? e.message : String(e) });
|
|
143
160
|
}
|
|
144
161
|
}
|
|
145
|
-
if (adapters.size < 2) {
|
|
162
|
+
if (adapters.size < 2 && unavailable.length === 0) {
|
|
146
163
|
console.error(chalk.red("\n Need at least 2 exchanges.\n"));
|
|
147
164
|
return;
|
|
148
165
|
}
|
|
149
|
-
const snapshots = await
|
|
166
|
+
const { snapshots, failed, notConfigured } = await fetchBalancesWithFailures(adapters);
|
|
167
|
+
unavailable.push(...failed);
|
|
168
|
+
// A venue without credentials holds nothing the CLI can move: left out (noted), not a failure.
|
|
169
|
+
if (!isJson())
|
|
170
|
+
for (const n of notConfigured)
|
|
171
|
+
console.log(chalk.gray(` ${n}: not configured (no credentials) — left out`));
|
|
172
|
+
// Moving funds on a plan computed over fewer venues than requested would mis-size every
|
|
173
|
+
// move: abort unless every requested venue was read.
|
|
174
|
+
if (unavailable.length > 0) {
|
|
175
|
+
throw new PerpError("EXCHANGE_ERROR", `Rebalance aborted: balances of ${unavailable.map(u => u.exchange).join(", ")} could not be read (${unavailable.map(u => `${u.exchange}: ${u.error}`).join("; ")})`, {
|
|
176
|
+
unavailable,
|
|
177
|
+
remediation: `Retry when every venue answers, or exclude the unavailable venue(s) explicitly with --exchanges ${exchangeNames.filter(n => !unavailable.some(u => u.exchange === n)).join(",") || "<list>"}`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
150
180
|
const plan = computeRebalancePlan(snapshots, {
|
|
151
181
|
minMove: parseFloat(opts.minMove),
|
|
152
182
|
reserve: parseFloat(opts.reserve),
|
|
@@ -238,6 +268,12 @@ export function registerFundsRebalanceCommands(parent, getAdapterForExchange, is
|
|
|
238
268
|
console.log(chalk.cyan(" Rebalance initiated. Monitor progress with 'perp funds rebalance check'.\n"));
|
|
239
269
|
});
|
|
240
270
|
}
|
|
271
|
+
function printUnavailable(unavailable) {
|
|
272
|
+
for (const u of unavailable)
|
|
273
|
+
console.log(chalk.yellow(` ⚠ ${u.exchange} unavailable (left out): ${u.error}`));
|
|
274
|
+
if (unavailable.length > 0)
|
|
275
|
+
console.log();
|
|
276
|
+
}
|
|
241
277
|
/**
|
|
242
278
|
* Execute a withdrawal from a source exchange.
|
|
243
279
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { QFEX_WITHDRAW_ENABLED } from "../exchanges/venues/qfex.js";
|
|
1
2
|
import { unsupported } from "../exchanges/trading-v2.js";
|
|
2
3
|
import chalk from "chalk";
|
|
3
4
|
import { printJson, jsonOk, withJsonErrors } from "../utils.js";
|
|
@@ -116,7 +117,7 @@ export function registerSettingsCommands(program, isJson, getAdapterForExchange)
|
|
|
116
117
|
const fee = "feeBps" in e ? `${e.feeBps} bps` : "fee share (you pay the normal fee)";
|
|
117
118
|
console.log(` ${v.padEnd(14)} ${id} ${fee}${BUILDER_FEE_CAPABILITIES[v]?.feeKind === "max" ? " (max)" : ""}`);
|
|
118
119
|
}
|
|
119
|
-
console.log(chalk.white.bold(
|
|
120
|
+
console.log(chalk.white.bold(`\n Withdraw gate (qfex: API-key withdrawals${QFEX_WITHDRAW_ENABLED ? "" : " — currently disabled in perp-cli"}; hyperliquid: withdraw / transfer to a non-owner address; off unless set):`));
|
|
120
121
|
const gates = Object.entries(s.withdraw);
|
|
121
122
|
if (gates.length === 0)
|
|
122
123
|
console.log(chalk.gray(` (none — gated withdrawals on ${WITHDRAW_GATE_VENUES.join(", ")} are refused; Hyperliquid to the owner's own address is always allowed)`));
|
package/dist/commands/trade.js
CHANGED
|
@@ -1938,7 +1938,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
|
|
|
1938
1938
|
// ── Recent Fills ──
|
|
1939
1939
|
trade
|
|
1940
1940
|
.command("fills [symbol]")
|
|
1941
|
-
.description("Recent trade fills, optionally filtered by symbol")
|
|
1941
|
+
.description("Recent trade fills, optionally filtered by symbol (Aster: last 7 days, symbols with a position, open order or fill in that window)")
|
|
1942
1942
|
.option("-l, --limit <n>", "Number of fills", "30")
|
|
1943
1943
|
.action(async (symbol, opts) => {
|
|
1944
1944
|
await withJsonErrors(isJson(), async () => {
|
|
@@ -2120,6 +2120,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
|
|
|
2120
2120
|
}
|
|
2121
2121
|
// Fetch funding payments (recent) for display
|
|
2122
2122
|
let fundingBySymbol = {};
|
|
2123
|
+
let fundingError = null;
|
|
2123
2124
|
try {
|
|
2124
2125
|
const payments = await adapter.getFundingPayments(50);
|
|
2125
2126
|
for (const fp of payments) {
|
|
@@ -2127,8 +2128,9 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
|
|
|
2127
2128
|
fundingBySymbol[sym] = (fundingBySymbol[sym] || 0) + parseFloat(fp.payment);
|
|
2128
2129
|
}
|
|
2129
2130
|
}
|
|
2130
|
-
catch {
|
|
2131
|
-
//
|
|
2131
|
+
catch (e) {
|
|
2132
|
+
// Shown under the table: funding columns are then unknown, not 0.
|
|
2133
|
+
fundingError = e instanceof Error ? e.message : String(e);
|
|
2132
2134
|
}
|
|
2133
2135
|
console.clear();
|
|
2134
2136
|
console.log(chalk.cyan.bold(`\n PnL Tracker — ${adapter.name} | ${new Date().toLocaleTimeString()}\n`));
|
|
@@ -2155,11 +2157,13 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
|
|
|
2155
2157
|
`$${formatUsd(p.markPrice)}`,
|
|
2156
2158
|
pnlColor(`${pnl >= 0 ? "+" : ""}$${pnl.toFixed(2)}`),
|
|
2157
2159
|
pctColor(`${pnlPct >= 0 ? "+" : ""}${pnlPct.toFixed(2)}%`),
|
|
2158
|
-
funding !== 0 ? `$${funding.toFixed(4)}` : "-",
|
|
2160
|
+
fundingError !== null ? chalk.yellow("n/a") : funding !== 0 ? `$${funding.toFixed(4)}` : "-",
|
|
2159
2161
|
];
|
|
2160
2162
|
});
|
|
2161
2163
|
console.log(makeTable(["Symbol", "Side", "Size", "Entry", "Mark", "PnL", "PnL%", "Funding"], rows));
|
|
2162
2164
|
}
|
|
2165
|
+
if (fundingError !== null)
|
|
2166
|
+
console.log(chalk.yellow(`\n Funding history unavailable (Funding column n/a): ${fundingError}`));
|
|
2163
2167
|
console.log(chalk.gray(`\n Refreshing every ${intervalSec}s... Press Ctrl+C to stop.`));
|
|
2164
2168
|
await new Promise(r => setTimeout(r, intervalSec * 1000));
|
|
2165
2169
|
}
|
package/dist/commands/wallet.js
CHANGED
|
@@ -114,7 +114,7 @@ function balanceTable(balances) {
|
|
|
114
114
|
b.chain,
|
|
115
115
|
chalk.white.bold(b.token),
|
|
116
116
|
b.balance === null ? chalk.red("read failed") : b.balance,
|
|
117
|
-
b.usdValue
|
|
117
|
+
b.usdValue !== null ? `$${formatUsd(b.usdValue)}` : b.priceError !== null ? chalk.gray("price unavailable") : chalk.gray("-"),
|
|
118
118
|
]);
|
|
119
119
|
const total = stableTotal(balances);
|
|
120
120
|
rows.push([chalk.gray("total"), chalk.white.bold("stablecoins"), "", `$${formatUsd(total.usd)}${total.errors.length > 0 ? chalk.red(" (some reads failed)") : ""}`]);
|
|
@@ -15,14 +15,25 @@ export interface ChainMarginStatus {
|
|
|
15
15
|
marginRatio: number;
|
|
16
16
|
belowThreshold: boolean;
|
|
17
17
|
}
|
|
18
|
+
/** A venue whose margin could not be read: UNKNOWN (never healthy) — callers block new entries on it. */
|
|
19
|
+
export interface ChainMarginUnknown {
|
|
20
|
+
exchange: string;
|
|
21
|
+
error: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ChainMarginCheck {
|
|
24
|
+
statuses: ChainMarginStatus[];
|
|
25
|
+
/** Venues whose balance read failed / returned non-numeric values (margin unknown). */
|
|
26
|
+
unknown: ChainMarginUnknown[];
|
|
27
|
+
}
|
|
18
28
|
/**
|
|
19
29
|
* Check margin status across all configured exchanges.
|
|
20
30
|
*
|
|
21
31
|
* @param adapters - Map of exchange name to adapter
|
|
22
32
|
* @param minMarginPct - Warn when free margin / equity falls below this (default: 30%)
|
|
23
|
-
* @returns Per-exchange margin status
|
|
33
|
+
* @returns Per-exchange margin status, plus every venue whose margin is unknown (a failed
|
|
34
|
+
* read is reported there — never dropped, so it can still block entries)
|
|
24
35
|
*/
|
|
25
|
-
export declare function checkChainMargins(adapters: Map<string, ExchangeAdapter>, minMarginPct?: number): Promise<
|
|
36
|
+
export declare function checkChainMargins(adapters: Map<string, ExchangeAdapter>, minMarginPct?: number): Promise<ChainMarginCheck>;
|
|
26
37
|
/**
|
|
27
38
|
* Check if a specific exchange has critically low margin (below 15%).
|
|
28
39
|
*/
|
|
@@ -31,6 +42,22 @@ export declare function isCriticalMargin(status: ChainMarginStatus): boolean;
|
|
|
31
42
|
* Check if a specific exchange should block new entries (below threshold).
|
|
32
43
|
*/
|
|
33
44
|
export declare function shouldBlockEntries(status: ChainMarginStatus, minMarginPct?: number): boolean;
|
|
45
|
+
/** Why new entries on a venue are blocked this cycle (closing / reducing stays allowed). */
|
|
46
|
+
export interface MarginEntryBlock {
|
|
47
|
+
exchange: string;
|
|
48
|
+
level: "critical" | "low" | "unknown";
|
|
49
|
+
/** null when the margin is unknown */
|
|
50
|
+
marginPct: number | null;
|
|
51
|
+
/** Threshold breached; null when unknown */
|
|
52
|
+
threshold: number | null;
|
|
53
|
+
/** Read error when unknown */
|
|
54
|
+
error?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Entry blocks from a margin check: critical (<15%), low (< minMarginPct), and UNKNOWN
|
|
58
|
+
* (failed read — never treated as healthy, so new entries are blocked on it too).
|
|
59
|
+
*/
|
|
60
|
+
export declare function marginEntryBlocks(check: ChainMarginCheck, minMarginPct?: number): MarginEntryBlock[];
|
|
34
61
|
/**
|
|
35
62
|
* Compute the optimal position size automatically based on:
|
|
36
63
|
* 1. Orderbook depth on both sides
|
|
@@ -17,7 +17,8 @@ const EXCHANGE_CHAIN_MAP = {
|
|
|
17
17
|
*
|
|
18
18
|
* @param adapters - Map of exchange name to adapter
|
|
19
19
|
* @param minMarginPct - Warn when free margin / equity falls below this (default: 30%)
|
|
20
|
-
* @returns Per-exchange margin status
|
|
20
|
+
* @returns Per-exchange margin status, plus every venue whose margin is unknown (a failed
|
|
21
|
+
* read is reported there — never dropped, so it can still block entries)
|
|
21
22
|
*/
|
|
22
23
|
export async function checkChainMargins(adapters, minMarginPct = 30) {
|
|
23
24
|
const entries = [...adapters.entries()];
|
|
@@ -25,6 +26,9 @@ export async function checkChainMargins(adapters, minMarginPct = 30) {
|
|
|
25
26
|
const bal = await adapter.getBalance();
|
|
26
27
|
const equity = Number(bal.equity);
|
|
27
28
|
const usedMargin = Number(bal.marginUsed);
|
|
29
|
+
if (!Number.isFinite(equity) || !Number.isFinite(usedMargin)) {
|
|
30
|
+
throw new Error(`balance returned non-numeric equity/marginUsed (${String(bal.equity)}/${String(bal.marginUsed)})`);
|
|
31
|
+
}
|
|
28
32
|
const freeMargin = equity - usedMargin;
|
|
29
33
|
// marginRatio = (freeMargin / equity) * 100
|
|
30
34
|
// If equity is 0, ratio is 0 (critical). If no margin used, ratio is 100.
|
|
@@ -39,9 +43,15 @@ export async function checkChainMargins(adapters, minMarginPct = 30) {
|
|
|
39
43
|
belowThreshold: marginRatio < minMarginPct,
|
|
40
44
|
};
|
|
41
45
|
}));
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
const statuses = [];
|
|
47
|
+
const unknown = [];
|
|
48
|
+
results.forEach((r, i) => {
|
|
49
|
+
if (r.status === "fulfilled")
|
|
50
|
+
statuses.push(r.value);
|
|
51
|
+
else
|
|
52
|
+
unknown.push({ exchange: entries[i][0], error: r.reason instanceof Error ? r.reason.message : String(r.reason) });
|
|
53
|
+
});
|
|
54
|
+
return { statuses, unknown };
|
|
45
55
|
}
|
|
46
56
|
/**
|
|
47
57
|
* Check if a specific exchange has critically low margin (below 15%).
|
|
@@ -55,6 +65,22 @@ export function isCriticalMargin(status) {
|
|
|
55
65
|
export function shouldBlockEntries(status, minMarginPct = 30) {
|
|
56
66
|
return status.marginRatio < minMarginPct;
|
|
57
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Entry blocks from a margin check: critical (<15%), low (< minMarginPct), and UNKNOWN
|
|
70
|
+
* (failed read — never treated as healthy, so new entries are blocked on it too).
|
|
71
|
+
*/
|
|
72
|
+
export function marginEntryBlocks(check, minMarginPct = 30) {
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const ms of check.statuses) {
|
|
75
|
+
if (isCriticalMargin(ms))
|
|
76
|
+
out.push({ exchange: ms.exchange, level: "critical", marginPct: ms.marginRatio, threshold: 15 });
|
|
77
|
+
else if (shouldBlockEntries(ms, minMarginPct))
|
|
78
|
+
out.push({ exchange: ms.exchange, level: "low", marginPct: ms.marginRatio, threshold: minMarginPct });
|
|
79
|
+
}
|
|
80
|
+
for (const u of check.unknown)
|
|
81
|
+
out.push({ exchange: u.exchange, level: "unknown", marginPct: null, threshold: null, error: u.error });
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
58
84
|
// ── Dynamic Auto-Sizing ──
|
|
59
85
|
/**
|
|
60
86
|
* Compute the optimal position size automatically based on:
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Polls all configured exchange adapters and pushes updates to connected clients.
|
|
5
5
|
* Includes cross-exchange arb data: funding rate comparison + dex arb scan.
|
|
6
6
|
*/
|
|
7
|
-
import type { ExchangeAdapter,
|
|
7
|
+
import type { ExchangeAdapter, ExchangeOrder, ExchangeMarketInfo } from "../exchanges/index.js";
|
|
8
|
+
import { type DashboardBalance, type DashboardPosition } from "./ws-feeds.js";
|
|
8
9
|
export interface DashboardExchange {
|
|
9
10
|
name: string;
|
|
10
11
|
adapter: ExchangeAdapter;
|
|
@@ -40,15 +41,40 @@ export interface DexAssetRow {
|
|
|
40
41
|
oi: number;
|
|
41
42
|
}[];
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
/**
|
|
45
|
+
* JSON shape served on /api/snapshot and pushed over the WS ("snapshot" messages).
|
|
46
|
+
*
|
|
47
|
+
* A venue part that could not be read is null — never a $0 balance / empty list:
|
|
48
|
+
* exchanges[].balance / positions / orders: null = unavailable, the reason is in `error`.
|
|
49
|
+
* exchanges[].balance.unrealizedPnl, positions[].entryPrice / markPrice / unrealizedPnl: null = unknown.
|
|
50
|
+
* exchanges[].feed: live-feed health (WS manager only) — stale = no fresh data (see isFeedStale).
|
|
51
|
+
* totals cover only the known values; totals.partial = some venue part was left out,
|
|
52
|
+
* totals.unavailable lists which ("<venue>.<part>").
|
|
53
|
+
*/
|
|
54
|
+
export interface DashboardExchangeData {
|
|
55
|
+
name: string;
|
|
56
|
+
balance: DashboardBalance | null;
|
|
57
|
+
positions: DashboardPosition[] | null;
|
|
58
|
+
orders: ExchangeOrder[] | null;
|
|
59
|
+
topMarkets: ExchangeMarketInfo[];
|
|
60
|
+
/** Why topMarkets could not be refreshed (the list shown is the last good read); null = fresh. */
|
|
61
|
+
marketsError: string | null;
|
|
62
|
+
/** Why a part is null / the feed is stale; null = everything read. */
|
|
63
|
+
error: string | null;
|
|
64
|
+
feed?: {
|
|
65
|
+
mode: "ws" | "rest" | "connecting";
|
|
66
|
+
lastOkAt: number | null;
|
|
67
|
+
lastError: string | null;
|
|
68
|
+
stale: boolean;
|
|
69
|
+
};
|
|
70
|
+
dexBalances?: {
|
|
46
71
|
name: string;
|
|
47
|
-
balance:
|
|
48
|
-
positions: ExchangePosition[];
|
|
49
|
-
orders: ExchangeOrder[];
|
|
50
|
-
topMarkets: ExchangeMarketInfo[];
|
|
72
|
+
balance: DashboardBalance | null;
|
|
51
73
|
}[];
|
|
74
|
+
}
|
|
75
|
+
export interface DashboardSnapshot {
|
|
76
|
+
timestamp: string;
|
|
77
|
+
exchanges: DashboardExchangeData[];
|
|
52
78
|
totals: {
|
|
53
79
|
equity: number;
|
|
54
80
|
available: number;
|
|
@@ -56,6 +82,10 @@ export interface DashboardSnapshot {
|
|
|
56
82
|
unrealizedPnl: number;
|
|
57
83
|
positionCount: number;
|
|
58
84
|
orderCount: number;
|
|
85
|
+
/** true = some venue part (or a stale feed) is not reflected in these sums. */
|
|
86
|
+
partial: boolean;
|
|
87
|
+
/** "<venue>.<part>" left out of the sums (balance, unrealizedPnl, positions, orders, stale). */
|
|
88
|
+
unavailable: string[];
|
|
59
89
|
};
|
|
60
90
|
arb: {
|
|
61
91
|
opportunities: ArbOpportunity[];
|
|
@@ -73,6 +103,13 @@ export interface DashboardOpts {
|
|
|
73
103
|
arbInterval?: number;
|
|
74
104
|
signal?: AbortSignal;
|
|
75
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* REST read of every venue. A failed read is null + its reason in `error` — never a $0
|
|
108
|
+
* balance or an empty position / order list (those would read as "flat, no money").
|
|
109
|
+
*/
|
|
110
|
+
export declare function readExchangesRest(exchanges: DashboardExchange[]): Promise<DashboardExchangeData[]>;
|
|
111
|
+
/** Merge HL dex entries + compute totals. Unknown (null) parts are skipped and listed in totals.unavailable. */
|
|
112
|
+
export declare function mergeAndTotal(exchangeData: DashboardExchangeData[]): DashboardSnapshot;
|
|
76
113
|
/**
|
|
77
114
|
* Start the dashboard HTTP + WebSocket server.
|
|
78
115
|
*/
|