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.
Files changed (75) hide show
  1. package/dist/arb/sizing.js +29 -7
  2. package/dist/bot/conditions.d.ts +5 -1
  3. package/dist/bot/conditions.js +16 -11
  4. package/dist/bot/engine.d.ts +12 -0
  5. package/dist/bot/engine.js +55 -44
  6. package/dist/bot/strategies/claude-agent.js +2 -2
  7. package/dist/bot/strategies/funding-arb-strategy.js +16 -20
  8. package/dist/bot/strategies/funding-arb-utils.d.ts +19 -0
  9. package/dist/bot/strategies/funding-arb-utils.js +44 -0
  10. package/dist/bot/strategies/funding-auto.js +61 -37
  11. package/dist/bot/strategies/grid-strategy.d.ts +2 -0
  12. package/dist/bot/strategies/grid-strategy.js +21 -2
  13. package/dist/bot/strategies/spot-perp-arb-strategy.js +28 -20
  14. package/dist/bot/tui/BotDashboard.d.ts +13 -2
  15. package/dist/bot/tui/BotDashboard.js +8 -3
  16. package/dist/bridge-engine.d.ts +2 -0
  17. package/dist/bridge-engine.js +68 -31
  18. package/dist/cli-program.js +19 -2
  19. package/dist/commands/account.js +2 -2
  20. package/dist/commands/arb/index.d.ts +20 -0
  21. package/dist/commands/arb/index.js +121 -36
  22. package/dist/commands/arb-auto.d.ts +36 -0
  23. package/dist/commands/arb-auto.js +202 -76
  24. package/dist/commands/arb.js +51 -35
  25. package/dist/commands/bot.d.ts +6 -0
  26. package/dist/commands/bot.js +55 -45
  27. package/dist/commands/funds-onchain.d.ts +1 -1
  28. package/dist/commands/funds-onchain.js +12 -49
  29. package/dist/commands/funds.js +51 -52
  30. package/dist/commands/history.js +165 -62
  31. package/dist/commands/portfolio.d.ts +5 -0
  32. package/dist/commands/portfolio.js +58 -20
  33. package/dist/commands/rebalance.js +46 -10
  34. package/dist/commands/settings.js +2 -1
  35. package/dist/commands/trade.js +8 -4
  36. package/dist/commands/wallet.js +74 -155
  37. package/dist/cross-chain-margin.d.ts +29 -2
  38. package/dist/cross-chain-margin.js +30 -4
  39. package/dist/dashboard/server.d.ts +45 -8
  40. package/dist/dashboard/server.js +113 -42
  41. package/dist/dashboard/ui.js +46 -27
  42. package/dist/dashboard/ws-feeds.d.ts +25 -3
  43. package/dist/dashboard/ws-feeds.js +219 -82
  44. package/dist/dex-asset-map.d.ts +23 -4
  45. package/dist/dex-asset-map.js +39 -13
  46. package/dist/exchanges/arcus/adapter.js +33 -6
  47. package/dist/exchanges/arcus/public.d.ts +6 -0
  48. package/dist/exchanges/arcus/public.js +12 -4
  49. package/dist/exchanges/aster.d.ts +33 -0
  50. package/dist/exchanges/aster.js +162 -55
  51. package/dist/exchanges/hyperliquid.d.ts +30 -0
  52. package/dist/exchanges/hyperliquid.js +159 -34
  53. package/dist/exchanges/lighter.d.ts +1 -1
  54. package/dist/exchanges/lighter.js +11 -9
  55. package/dist/exchanges/ondo/adapter.d.ts +4 -18
  56. package/dist/exchanges/ondo/adapter.js +5 -29
  57. package/dist/exchanges/ondo/funds.d.ts +1 -29
  58. package/dist/exchanges/ondo/funds.js +2 -94
  59. package/dist/exchanges/pacifica.d.ts +12 -0
  60. package/dist/exchanges/pacifica.js +78 -35
  61. package/dist/exchanges/qfex/adapter.js +4 -1
  62. package/dist/exchanges/venues/ondo.d.ts +0 -5
  63. package/dist/exchanges/venues/ondo.js +2 -6
  64. package/dist/exchanges/venues/qfex.d.ts +11 -0
  65. package/dist/exchanges/venues/qfex.js +10 -2
  66. package/dist/mcp-server.js +2 -2
  67. package/dist/onchain-balances.d.ts +40 -0
  68. package/dist/onchain-balances.js +173 -0
  69. package/dist/rebalance.d.ts +20 -1
  70. package/dist/rebalance.js +31 -5
  71. package/dist/strategies/funding-arb.js +38 -7
  72. package/package.json +1 -1
  73. package/skills/perp-cli/SKILL.md +20 -3
  74. package/skills/perp-cli/references/agent-operations.md +2 -2
  75. 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 { fetchAllBalances, computeRebalancePlan, describeBridgeRoute, estimateMoveTime, } from "../rebalance.js";
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 fetchAllBalances(adapters);
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 fetchAllBalances(adapters);
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
- // skip
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 fetchAllBalances(adapters);
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("\n Withdraw gate (qfex: API-key withdrawals; hyperliquid: withdraw / transfer to a non-owner address; off unless set):"));
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)`));
@@ -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
- // funding payments may not be supported on all exchanges
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
  }
@@ -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
- const { Connection, PublicKey } = await import("@solana/web3.js");
103
- const rpcUrl = isTestnet
104
- ? "https://api.devnet.solana.com"
105
- : "https://api.mainnet-beta.solana.com";
106
- const connection = new Connection(rpcUrl);
107
- const pubkey = new PublicKey(address);
108
- const balances = [];
109
- const solLamports = await connection.getBalance(pubkey);
110
- const solBalance = solLamports / 1e9;
111
- balances.push({ token: "SOL", balance: solBalance.toFixed(6), usdValue: "" });
112
- const usdcMint = isTestnet
113
- ? "USDPqRbLidFGufty2s3oizmDEKdqx7ePTqzDMbf5ZKM"
114
- : "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
115
- try {
116
- const tokenAccounts = await connection.getParsedTokenAccountsByOwner(pubkey, {
117
- mint: new PublicKey(usdcMint),
118
- });
119
- let usdcBalance = 0;
120
- for (const { account } of tokenAccounts.value) {
121
- const parsed = account.data.parsed?.info;
122
- if (parsed)
123
- usdcBalance += Number(parsed.tokenAmount?.uiAmount ?? 0);
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
- balances.push({
126
- token: isTestnet ? "USDP" : "USDC",
127
- balance: usdcBalance.toFixed(2),
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
- catch {
140
- balances[0].usdValue = "-";
141
- }
142
- return balances;
144
+ return out;
143
145
  }
144
- async function getEvmBalances(address, isTestnet) {
145
- const { ethers } = await import("ethers");
146
- const rpcUrl = isTestnet
147
- ? "https://sepolia-rollup.arbitrum.io/rpc"
148
- : "https://arb1.arbitrum.io/rpc";
149
- const provider = new ethers.JsonRpcProvider(rpcUrl);
150
- const balances = [];
151
- const ethBal = await provider.getBalance(address);
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
- return balances;
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("Check on-chain balance for a saved wallet (or active wallet)")
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
- const ows = loadOws();
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 — fall through to env path
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
- for (const we of envEntries) {
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
- console.log();
965
- return;
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
- if (!isJson())
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
- const rows = balances.map((b) => [chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-")]);
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
- const rows = balances.map((b) => [chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-")]);
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 array
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<ChainMarginStatus[]>;
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 array
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
- return results
43
- .filter((r) => r.status === "fulfilled")
44
- .map((r) => r.value);
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: