perp-cli 0.14.0 → 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/cli-program.js +19 -2
- 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.d.ts +1 -1
- package/dist/commands/funds-onchain.js +12 -49
- package/dist/commands/funds.js +51 -52
- package/dist/commands/history.js +165 -62
- 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 +74 -155
- 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 +33 -0
- package/dist/exchanges/aster.js +162 -55
- 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/adapter.d.ts +4 -18
- package/dist/exchanges/ondo/adapter.js +5 -29
- package/dist/exchanges/ondo/funds.d.ts +1 -29
- package/dist/exchanges/ondo/funds.js +2 -94
- package/dist/exchanges/pacifica.d.ts +12 -0
- package/dist/exchanges/pacifica.js +78 -35
- package/dist/exchanges/qfex/adapter.js +4 -1
- package/dist/exchanges/venues/ondo.d.ts +0 -5
- package/dist/exchanges/venues/ondo.js +2 -6
- package/dist/exchanges/venues/qfex.d.ts +11 -0
- package/dist/exchanges/venues/qfex.js +10 -2
- package/dist/mcp-server.js +2 -2
- package/dist/onchain-balances.d.ts +40 -0
- package/dist/onchain-balances.js +173 -0
- 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 +20 -3
- package/skills/perp-cli/references/agent-operations.md +2 -2
- package/skills/perp-cli/references/commands.md +12 -9
|
@@ -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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Option } from "commander";
|
|
2
|
+
import { evmOnchainBalances, solanaOnchainBalances, stableTotal } from "../onchain-balances.js";
|
|
2
3
|
import { PerpError } from "../errors.js";
|
|
3
4
|
import chalk from "chalk";
|
|
4
5
|
import bs58 from "bs58";
|
|
@@ -98,80 +99,60 @@ export function getWalletKeyByName(name) {
|
|
|
98
99
|
const store = loadStore();
|
|
99
100
|
return store.wallets[name]?.privateKey ?? null;
|
|
100
101
|
}
|
|
102
|
+
/** SOL + USDC / USDT on Solana (devnet: USDP). */
|
|
101
103
|
async function getSolanaBalances(address, isTestnet) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
104
|
+
return solanaOnchainBalances(address, isTestnet);
|
|
105
|
+
}
|
|
106
|
+
/** Gas token + USDC / USDT (USDG on Robinhood Chain) on every EVM chain perp-cli uses; `only` limits the chains. */
|
|
107
|
+
async function getEvmBalances(address, isTestnet, only) {
|
|
108
|
+
return evmOnchainBalances(address, isTestnet, only);
|
|
109
|
+
}
|
|
110
|
+
/** Human table: non-zero balances and failed reads (JSON keeps every row), then the stablecoin total. */
|
|
111
|
+
function balanceTable(balances) {
|
|
112
|
+
const shown = balances.filter(b => b.error !== null || Number(b.balance) !== 0);
|
|
113
|
+
const rows = shown.map((b) => [
|
|
114
|
+
b.chain,
|
|
115
|
+
chalk.white.bold(b.token),
|
|
116
|
+
b.balance === null ? chalk.red("read failed") : b.balance,
|
|
117
|
+
b.usdValue !== null ? `$${formatUsd(b.usdValue)}` : b.priceError !== null ? chalk.gray("price unavailable") : chalk.gray("-"),
|
|
118
|
+
]);
|
|
119
|
+
const total = stableTotal(balances);
|
|
120
|
+
rows.push([chalk.gray("total"), chalk.white.bold("stablecoins"), "", `$${formatUsd(total.usd)}${total.errors.length > 0 ? chalk.red(" (some reads failed)") : ""}`]);
|
|
121
|
+
return makeTable(["Chain", "Token", "Balance", "USD Value"], rows);
|
|
122
|
+
}
|
|
123
|
+
async function balanceEntry(label, vm, address, testnet) {
|
|
124
|
+
const balances = vm === "solana" ? await getSolanaBalances(address, testnet) : await getEvmBalances(address, testnet);
|
|
125
|
+
const total = stableTotal(balances);
|
|
126
|
+
return { ...label, chain: vm, address, balances, stableUsd: total.usd, errors: total.errors };
|
|
127
|
+
}
|
|
128
|
+
/** Reads each distinct address once (several venues often share one key), in order. */
|
|
129
|
+
async function balanceEntries(items, testnet) {
|
|
130
|
+
const seen = new Map();
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const it of items) {
|
|
133
|
+
const key = `${it.vm}:${it.address.toLowerCase()}`;
|
|
134
|
+
const prev = seen.get(key);
|
|
135
|
+
if (prev !== undefined) {
|
|
136
|
+
if (it.label.exchange !== undefined)
|
|
137
|
+
prev.exchange = prev.exchange === undefined ? it.label.exchange : `${prev.exchange},${it.label.exchange}`;
|
|
138
|
+
continue;
|
|
124
139
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
usdValue: `$${formatUsd(usdcBalance)}`,
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
balances.push({ token: isTestnet ? "USDP" : "USDC", balance: "0.00", usdValue: "$0.00" });
|
|
133
|
-
}
|
|
134
|
-
try {
|
|
135
|
-
const res = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd");
|
|
136
|
-
const json = (await res.json());
|
|
137
|
-
balances[0].usdValue = `$${formatUsd(solBalance * (json.solana?.usd ?? 0))}`;
|
|
140
|
+
const entry = await balanceEntry(it.label, it.vm, it.address, testnet);
|
|
141
|
+
seen.set(key, entry);
|
|
142
|
+
out.push(entry);
|
|
138
143
|
}
|
|
139
|
-
|
|
140
|
-
balances[0].usdValue = "-";
|
|
141
|
-
}
|
|
142
|
-
return balances;
|
|
144
|
+
return out;
|
|
143
145
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const ethBalance = Number(ethers.formatEther(ethBal));
|
|
153
|
-
balances.push({ token: "ETH", balance: ethBalance.toFixed(6), usdValue: "" });
|
|
154
|
-
const usdcAddr = isTestnet
|
|
155
|
-
? "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d"
|
|
156
|
-
: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";
|
|
157
|
-
try {
|
|
158
|
-
const usdc = new ethers.Contract(usdcAddr, ["function balanceOf(address) view returns (uint256)"], provider);
|
|
159
|
-
const rawBal = await usdc.balanceOf(address);
|
|
160
|
-
const usdcBalance = Number(ethers.formatUnits(rawBal, 6));
|
|
161
|
-
balances.push({ token: "USDC", balance: usdcBalance.toFixed(2), usdValue: `$${formatUsd(usdcBalance)}` });
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
balances.push({ token: "USDC", balance: "0.00", usdValue: "$0.00" });
|
|
165
|
-
}
|
|
166
|
-
try {
|
|
167
|
-
const res = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd");
|
|
168
|
-
const json = (await res.json());
|
|
169
|
-
balances[0].usdValue = `$${formatUsd(ethBalance * (json.ethereum?.usd ?? 0))}`;
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
balances[0].usdValue = "-";
|
|
146
|
+
/** Exactly one JSON document (an array of entries), or one table per entry. */
|
|
147
|
+
function printBalanceEntries(entries, json) {
|
|
148
|
+
if (json)
|
|
149
|
+
return printJson(jsonOk(entries));
|
|
150
|
+
for (const e of entries) {
|
|
151
|
+
const who = [e.wallet, e.exchange].filter(x => x !== undefined).join(" / ");
|
|
152
|
+
console.log(chalk.cyan.bold(`\n ${who} (${e.chain} ${e.address.slice(0, 8)}...)`));
|
|
153
|
+
console.log(balanceTable(e.balances));
|
|
173
154
|
}
|
|
174
|
-
|
|
155
|
+
console.log();
|
|
175
156
|
}
|
|
176
157
|
// ── Derive address from private key ──────────────────────────
|
|
177
158
|
async function deriveSolanaAddress(key) {
|
|
@@ -816,7 +797,7 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
816
797
|
// ── balance (by wallet name) ──
|
|
817
798
|
wallet
|
|
818
799
|
.command("balance [name]")
|
|
819
|
-
.description("
|
|
800
|
+
.description("On-chain balances for a saved wallet (or the active one): gas token + USDC / USDT on Arbitrum, Ethereum, Base, BNB Chain, Robinhood Chain (USDG) and Solana")
|
|
820
801
|
.option("--testnet", "Use testnet", false)
|
|
821
802
|
.action(async (name, opts) => {
|
|
822
803
|
const store = loadStore();
|
|
@@ -867,44 +848,25 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
867
848
|
if (activeEntries.length === 0) {
|
|
868
849
|
const settings = loadSettings();
|
|
869
850
|
if (settings.owsActiveWallet) {
|
|
851
|
+
// Only loading the OWS vault may fall through to the env path; balance reads report
|
|
852
|
+
// their own per-token errors and any other failure propagates.
|
|
853
|
+
let owsAccounts = null;
|
|
870
854
|
try {
|
|
871
855
|
const { loadOws } = await import("../signer/ows-loader.js");
|
|
872
|
-
|
|
873
|
-
const owsWallet = ows.getWallet(settings.owsActiveWallet);
|
|
874
|
-
const owsResults = [];
|
|
875
|
-
for (const acct of owsWallet.accounts) {
|
|
876
|
-
const isEvm = acct.chainId.startsWith("eip155:");
|
|
877
|
-
const isSol = acct.chainId.startsWith("solana:");
|
|
878
|
-
if (!isEvm && !isSol)
|
|
879
|
-
continue;
|
|
880
|
-
const address = acct.address;
|
|
881
|
-
const balances = isSol
|
|
882
|
-
? await getSolanaBalances(address, opts.testnet)
|
|
883
|
-
: await getEvmBalances(address, opts.testnet);
|
|
884
|
-
if (isJson()) {
|
|
885
|
-
owsResults.push({
|
|
886
|
-
wallet: settings.owsActiveWallet,
|
|
887
|
-
chain: isSol ? "solana" : "evm",
|
|
888
|
-
address,
|
|
889
|
-
balances,
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
|
-
else {
|
|
893
|
-
console.log(chalk.cyan.bold(`\n ${settings.owsActiveWallet} (${isSol ? "solana" : "evm"} ${address.slice(0, 8)}...)`));
|
|
894
|
-
const rows = balances.map((b) => [
|
|
895
|
-
chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-"),
|
|
896
|
-
]);
|
|
897
|
-
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
if (isJson())
|
|
901
|
-
printJson(jsonOk(owsResults));
|
|
902
|
-
else
|
|
903
|
-
console.log();
|
|
904
|
-
return;
|
|
856
|
+
owsAccounts = loadOws().getWallet(settings.owsActiveWallet).accounts;
|
|
905
857
|
}
|
|
906
858
|
catch {
|
|
907
|
-
// OWS module unavailable or vault missing —
|
|
859
|
+
owsAccounts = null; // OWS module unavailable or vault missing — use the env path below
|
|
860
|
+
}
|
|
861
|
+
if (owsAccounts !== null) {
|
|
862
|
+
const results = [];
|
|
863
|
+
for (const acct of owsAccounts) {
|
|
864
|
+
const vm = acct.chainId.startsWith("solana:") ? "solana" : acct.chainId.startsWith("eip155:") ? "evm" : null;
|
|
865
|
+
if (vm === null)
|
|
866
|
+
continue;
|
|
867
|
+
results.push(await balanceEntry({ wallet: settings.owsActiveWallet }, vm, acct.address, opts.testnet));
|
|
868
|
+
}
|
|
869
|
+
return printBalanceEntries(results, isJson());
|
|
908
870
|
}
|
|
909
871
|
}
|
|
910
872
|
const stored = loadEnvFile();
|
|
@@ -928,54 +890,13 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
928
890
|
}
|
|
929
891
|
process.exit(1);
|
|
930
892
|
}
|
|
931
|
-
|
|
932
|
-
if (!isJson())
|
|
933
|
-
console.log(chalk.cyan.bold(`\n ${we.exchange} (${we.address.slice(0, 8)}...)`));
|
|
934
|
-
const balances = we.chain === "solana"
|
|
935
|
-
? await getSolanaBalances(we.address, opts.testnet)
|
|
936
|
-
: await getEvmBalances(we.address, opts.testnet);
|
|
937
|
-
if (isJson()) {
|
|
938
|
-
printJson(jsonOk({ exchange: we.exchange, address: we.address, balances }));
|
|
939
|
-
continue;
|
|
940
|
-
}
|
|
941
|
-
const rows = balances.map((b) => [
|
|
942
|
-
chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-"),
|
|
943
|
-
]);
|
|
944
|
-
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
945
|
-
}
|
|
946
|
-
console.log();
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
for (const ae of activeEntries) {
|
|
950
|
-
if (!isJson())
|
|
951
|
-
console.log(chalk.cyan.bold(`\n ${ae.name} (${ae.exchange})`));
|
|
952
|
-
const balances = ae.type === "solana"
|
|
953
|
-
? await getSolanaBalances(ae.address, opts.testnet)
|
|
954
|
-
: await getEvmBalances(ae.address, opts.testnet);
|
|
955
|
-
if (isJson()) {
|
|
956
|
-
printJson(jsonOk({ wallet: ae.name, exchange: ae.exchange, balances }));
|
|
957
|
-
continue;
|
|
958
|
-
}
|
|
959
|
-
const rows = balances.map((b) => [
|
|
960
|
-
chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-"),
|
|
961
|
-
]);
|
|
962
|
-
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
893
|
+
return printBalanceEntries(await balanceEntries(envEntries.map(we => ({ label: { exchange: we.exchange }, vm: we.chain, address: we.address })), opts.testnet), isJson());
|
|
963
894
|
}
|
|
964
|
-
|
|
965
|
-
|
|
895
|
+
return printBalanceEntries(await balanceEntries(activeEntries.map(ae => ({
|
|
896
|
+
label: { wallet: ae.name, exchange: ae.exchange }, vm: ae.type === "solana" ? "solana" : "evm", address: ae.address,
|
|
897
|
+
})), opts.testnet), isJson());
|
|
966
898
|
}
|
|
967
|
-
|
|
968
|
-
console.log(chalk.cyan(`\n Fetching balance for "${entry.name}" (${entry.address.slice(0, 8)}...)\n`));
|
|
969
|
-
const balances = entry.type === "solana"
|
|
970
|
-
? await getSolanaBalances(entry.address, opts.testnet)
|
|
971
|
-
: await getEvmBalances(entry.address, opts.testnet);
|
|
972
|
-
if (isJson())
|
|
973
|
-
return printJson(jsonOk({ wallet: entry.name, balances }));
|
|
974
|
-
const rows = balances.map((b) => [
|
|
975
|
-
chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-"),
|
|
976
|
-
]);
|
|
977
|
-
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
978
|
-
console.log();
|
|
899
|
+
return printBalanceEntries([await balanceEntry({ wallet: entry.name }, entry.type === "solana" ? "solana" : "evm", entry.address, opts.testnet)], isJson());
|
|
979
900
|
});
|
|
980
901
|
// ── direct address balance (kept for convenience) ──
|
|
981
902
|
wallet
|
|
@@ -988,8 +909,7 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
988
909
|
const balances = await getSolanaBalances(address, opts.testnet);
|
|
989
910
|
if (isJson())
|
|
990
911
|
return printJson(jsonOk(balances));
|
|
991
|
-
|
|
992
|
-
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
912
|
+
console.log(balanceTable(balances));
|
|
993
913
|
console.log(chalk.gray(`\n Network: ${opts.testnet ? "Devnet" : "Mainnet"}\n`));
|
|
994
914
|
});
|
|
995
915
|
wallet
|
|
@@ -999,11 +919,10 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
999
919
|
.action(async (address, opts) => {
|
|
1000
920
|
if (!isJson())
|
|
1001
921
|
console.log(chalk.cyan(`\n Fetching Arbitrum balances for ${address.slice(0, 8)}...${address.slice(-4)}\n`));
|
|
1002
|
-
const balances = await getEvmBalances(address, opts.testnet);
|
|
922
|
+
const balances = await getEvmBalances(address, opts.testnet, [opts.testnet ? "arbitrum-sepolia" : "arbitrum"]);
|
|
1003
923
|
if (isJson())
|
|
1004
924
|
return printJson(jsonOk(balances));
|
|
1005
|
-
|
|
1006
|
-
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
925
|
+
console.log(balanceTable(balances));
|
|
1007
926
|
console.log(chalk.gray(`\n Network: Arbitrum ${opts.testnet ? "Sepolia" : "One"}\n`));
|
|
1008
927
|
});
|
|
1009
928
|
// ── migrate (legacy → OWS) ──
|
|
@@ -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:
|