perp-cli 0.14.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -195,8 +195,25 @@ export function buildProgram(opts) {
195
195
  function getExchange() {
196
196
  return resolveVenueId(program.opts().exchange);
197
197
  }
198
+ /**
199
+ * The `--dex` value for the single `-e` venue. HIP-3 dexes exist only on Hyperliquid, so a
200
+ * `--dex` with another `-e` venue is refused instead of being dropped (the command would
201
+ * otherwise read / trade that venue's own markets while the user asked for a HIP-3 dex).
202
+ * Multi-venue commands (arb, portfolio) apply `--dex` to their Hyperliquid leg only and do
203
+ * not go through this check.
204
+ */
205
+ function dexForExchange() {
206
+ const dex = program.opts().dex ?? null;
207
+ if (dex !== null && getExchange() !== "hyperliquid") {
208
+ const venue = program.getOptionValueSource("exchange") === "cli" ? `-e ${getExchange()}` : `the default exchange (${getExchange()})`;
209
+ throw new PerpError("INVALID_PARAMS", `--dex ${dex} is a Hyperliquid HIP-3 dex; ${venue} has no HIP-3 dexes`, {
210
+ remediation: `Use -e hyperliquid --dex ${dex}, or drop --dex for ${getExchange()}.`,
211
+ });
212
+ }
213
+ return dex;
214
+ }
198
215
  async function getAdapter() {
199
- _adapter = await createVenueAdapter(getExchange(), program.opts().dex ?? null);
216
+ _adapter = await createVenueAdapter(getExchange(), dexForExchange());
200
217
  return _adapter;
201
218
  }
202
219
  // Sync wrapper for commands that need adapter (lazy init)
@@ -220,7 +237,7 @@ export function buildProgram(opts) {
220
237
  }
221
238
  // Public market-data adapters (read-only; no credential resolution).
222
239
  async function getPublicAdapter() {
223
- return createVenueAdapter(getExchange(), program.opts().dex ?? null, true);
240
+ return createVenueAdapter(getExchange(), dexForExchange(), true);
224
241
  }
225
242
  async function getPublicAdapterForExchange(rawExchange) {
226
243
  return createVenueAdapter(rawExchange, program.opts().dex ?? null, true);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `funds deposit aster|ondo`, `funds withdraw aster`, `funds address-book add ondo` — fund movements
2
+ * `funds deposit aster|ondo`, `funds withdraw aster` — fund movements
3
3
  * whose on-chain leg is signed by the OWS master wallet (src/evm-master-tx.ts).
4
4
  *
5
5
  * Every command: `--dry-run` prints the plan and returns before any key, adapter or network is
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `funds deposit aster|ondo`, `funds withdraw aster`, `funds address-book add ondo` — fund movements
2
+ * `funds deposit aster|ondo`, `funds withdraw aster` — fund movements
3
3
  * whose on-chain leg is signed by the OWS master wallet (src/evm-master-tx.ts).
4
4
  *
5
5
  * Every command: `--dry-run` prints the plan and returns before any key, adapter or network is
@@ -12,7 +12,7 @@ import { cliCredentialOptsFrom } from "../config.js";
12
12
  import { PerpError, parsePositiveDecimal } from "../errors.js";
13
13
  import { logExecution } from "../execution-log.js";
14
14
  import { unsupported } from "../exchanges/trading-v2.js";
15
- import { getVenue, networkConfig } from "../exchanges/venue.js";
15
+ import { getVenue } from "../exchanges/venue.js";
16
16
  import { ASTER_FUNDS_CHAINS } from "../exchanges/venues/aster.js";
17
17
  import { ONDO_DEPOSIT_CHAINS } from "../exchanges/venues/ondo.js";
18
18
  import { jsonOk, printJson } from "../utils.js";
@@ -235,52 +235,14 @@ export function registerOnchainFundsCommands(ctx) {
235
235
  }
236
236
  });
237
237
  // ═══ Address book (Ondo withdrawal destinations) ═══
238
- const addressBook = funds.command("address-book").description("Withdrawal address book (Ondo Perps only withdraws to listed addresses)");
238
+ // Ondo Perps withdrawals are web-app only (the API does not allow them), so an address-book
239
+ // entry made here would serve nothing. The command stays so the refusal is explicit.
240
+ const addressBook = funds.command("address-book").description("Not available: the Ondo Perps withdrawal address book is managed in the web app");
239
241
  supportsDryRun(addressBook.command("add <venue> <address>"))
240
- .description("Add the master wallet's own address to the Ondo Perps withdrawal address book (SIWE challenge signed by the OWS master wallet; supports --dry-run)")
241
- .option("--label <text>", "Label of the address book entry")
242
- .action(async (venue, address, opts) => {
242
+ .description("Not available: Ondo Perps withdrawals and their address book are web-app only (refused, also with --dry-run)")
243
+ .option("--label <text>", "Ignored")
244
+ .action(async (venue) => {
243
245
  const def = getVenue(venue);
244
- if (def.id !== "ondo") {
245
- throw unsupported(def.displayName, "address-book", "only Ondo Perps has a withdrawal address book", "perp funds address-book add ondo <address>");
246
- }
247
- const net = getNetwork();
248
- if (!/^0x[0-9a-fA-F]{40}$/.test(address))
249
- throw new PerpError("INVALID_PARAMS", `"${address}" is not an EVM address`);
250
- const plan = {
251
- venue: "ondo", network: net, address, label: opts.label === undefined ? null : opts.label,
252
- steps: "POST /v1/auth/erc-4361/address_book/get_challenge → personal_sign with the OWS master wallet → POST …/complete_challenge",
253
- };
254
- if (isDryRun()) {
255
- if (isJson())
256
- return printJson(jsonOk({ dryRun: true, sent: false, action: "address-book add", ...plan }));
257
- console.log(chalk.yellow(`\n [dry-run] Would add ${address} to the Ondo Perps address book (${net})`));
258
- console.log(` steps: ${plan.steps}`);
259
- console.log(chalk.gray(" Nothing was signed or sent.\n"));
260
- return;
261
- }
262
- const { loadMasterEvmSigner } = await import("../evm-master-tx.js");
263
- const signer = await loadMasterEvmSigner("ondo", creds(), "Adding an Ondo address book entry");
264
- if (signer.getAddress().toLowerCase() !== address.toLowerCase()) {
265
- throw new PerpError("INVALID_PARAMS", `The CLI only adds the master wallet's own address (${signer.getAddress()}) to the Ondo address book; ${address} is a different address`, {
266
- remediation: "Add other addresses in the Ondo Perps web app (Withdraw → address book)",
267
- });
268
- }
269
- const { OndoAdapter } = await import("../exchanges/ondo/adapter.js");
270
- const adapter = await getAdapterForExchange("ondo");
271
- if (!(adapter instanceof OndoAdapter))
272
- throw new Error("ondo venue did not produce an OndoAdapter");
273
- const { addOndoAddressBookEntry } = await import("../exchanges/ondo/funds.js");
274
- const res = await addOndoAddressBookEntry(adapter.restClient, {
275
- wallet: signer.getAddress(), restUrl: networkConfig(def, net).restUrl, label: plan.label, signMessage: m => signer.signMessage(m),
276
- });
277
- if (isJson())
278
- return printJson(jsonOk({ venue: "ondo", network: net, ...res }));
279
- if (res.alreadyListed)
280
- console.log(chalk.green(`\n ${res.address} is already on the Ondo Perps address book.\n`));
281
- else if (res.listed)
282
- console.log(chalk.green(`\n Added ${res.address} to the Ondo Perps address book.\n`));
283
- else
284
- console.log(chalk.yellow(`\n Ondo accepted the challenge, but ${res.address} is not listed yet (a cooldown may apply). Check the Ondo Perps app before withdrawing.\n`));
246
+ throw unsupported(def.displayName, "address-book", "the Ondo Perps API does not allow withdrawals, so the CLI keeps no withdrawal address book", "Manage withdrawals and the address book in the Ondo Perps web app");
285
247
  });
286
248
  }
@@ -10,8 +10,7 @@ import { cliCredentialOptsFrom, requireMasterWallet } from "../config.js";
10
10
  import { getVenue, requireCapabilities, READ_ONLY_CREDENTIALS } from "../exchanges/venue.js";
11
11
  import { unsupported } from "../exchanges/trading-v2.js";
12
12
  import { registerOnchainFundsCommands } from "./funds-onchain.js";
13
- import { ONDO_WITHDRAW_NETWORK } from "../exchanges/venues/ondo.js";
14
- import { QFEX_BANKER_NETWORK, QFEX_WITHDRAW_NETWORK } from "../exchanges/venues/qfex.js";
13
+ import { QFEX_BANKER_NETWORK, QFEX_WITHDRAW_DISABLED, QFEX_WITHDRAW_ENABLED, QFEX_WITHDRAW_NETWORK } from "../exchanges/venues/qfex.js";
15
14
  import { loadSettings } from "../settings.js";
16
15
  import { assertWithdrawAllowed, forgetWithdrawAttempt, recordWithdrawAttempt } from "../withdraw-gate.js";
17
16
  import "../exchanges/venues/index.js";
@@ -105,10 +104,9 @@ function withdrawAssetOf(def) {
105
104
  }
106
105
  /** Withdrawal chain of venues that settle off-chain and pay out on a public chain. */
107
106
  const OFFCHAIN_WITHDRAW_CHAIN = {
108
- ondo: ONDO_WITHDRAW_NETWORK,
109
107
  qfex: QFEX_WITHDRAW_NETWORK,
110
108
  };
111
- /** Chain a venue's withdrawals arrive on (Ondo / QFEX settle off-chain and pay out on Ethereum / Arbitrum). */
109
+ /** Chain a venue's withdrawals arrive on (QFEX settles off-chain and pays out on Arbitrum). */
112
110
  function withdrawChainOf(def, net) {
113
111
  const payout = OFFCHAIN_WITHDRAW_CHAIN[def.id];
114
112
  if (payout !== undefined)
@@ -1044,14 +1042,20 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
1044
1042
  throw err;
1045
1043
  }
1046
1044
  });
1047
- // ── Ondo Perps / Arcus — uniform surface (design §13.1): same <amount> + --to,
1045
+ // ── Arcus — uniform surface (design §13.1): same <amount> + --to,
1048
1046
  // same jsonOk(result), global --dry-run plans without sending. ──
1049
- for (const venueId of ["ondo", "arcus"]) {
1047
+ // Ondo Perps: the API does not allow withdrawals (address-book destinations only; adding one is
1048
+ // refused for API keys). The command stays so the refusal is explicit — also under --dry-run.
1049
+ supportsDryRun(withdraw.command("ondo [amount]"))
1050
+ .description("Not available: Ondo Perps withdrawals are web-app only (refused, also with --dry-run)")
1051
+ .option("--to <address>", "Ignored — the API cannot withdraw")
1052
+ .action(async () => {
1053
+ throw unsupported("Ondo Perps", "withdraw", "the Ondo Perps API does not allow withdrawals", "Withdraw from the Ondo Perps web app");
1054
+ });
1055
+ for (const venueId of ["arcus"]) {
1050
1056
  const def = getVenue(venueId);
1051
1057
  const asset = withdrawAssetOf(def);
1052
- const toHelp = venueId === "ondo"
1053
- ? "Destination EVM address (required; must be on the Ondo address book — see: perp funds address-book add ondo <address>)"
1054
- : "Destination EVM address (must equal the Arcus account owner — the default)";
1058
+ const toHelp = "Destination EVM address (must equal the Arcus account owner — the default)";
1055
1059
  supportsDryRun(withdraw.command(`${venueId} <amount>`))
1056
1060
  .description(`Withdraw ${asset} from ${def.displayName} to your ${chainLabel(withdrawChainOf(def, "mainnet"))} wallet (supports --dry-run)`)
1057
1061
  .option("--to <address>", toHelp)
@@ -1065,27 +1069,16 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
1065
1069
  const adapter = await getAdapterForExchange(venueId);
1066
1070
  if (!isWithdrawCapable(adapter))
1067
1071
  throw new Error(`${def.id} adapter has no withdraw()`);
1068
- let destination;
1069
- if (venueId === "ondo") {
1070
- if (opts.to === undefined) {
1071
- throw new PerpError("INVALID_PARAMS", "Ondo Perps withdraw needs a destination address (the API key has no owner address)", {
1072
- remediation: `perp funds withdraw ondo ${amount} --to <0x address on your Ondo address book>`,
1073
- });
1074
- }
1075
- destination = opts.to;
1076
- }
1077
- else {
1078
- const { ArcusAdapter } = await import("../exchanges/arcus/adapter.js");
1079
- if (!(adapter instanceof ArcusAdapter))
1080
- throw new Error("arcus venue did not produce an ArcusAdapter");
1081
- const owner = adapter.withdrawDestination;
1082
- if (opts.to !== undefined && opts.to.toLowerCase() !== owner.toLowerCase()) {
1083
- throw new PerpError("INVALID_PARAMS", `Arcus only withdraws to the account owner ${owner}`, {
1084
- remediation: `Omit --to (defaults to ${owner})`,
1085
- });
1086
- }
1087
- destination = owner;
1072
+ const { ArcusAdapter } = await import("../exchanges/arcus/adapter.js");
1073
+ if (!(adapter instanceof ArcusAdapter))
1074
+ throw new Error("arcus venue did not produce an ArcusAdapter");
1075
+ const owner = adapter.withdrawDestination;
1076
+ if (opts.to !== undefined && opts.to.toLowerCase() !== owner.toLowerCase()) {
1077
+ throw new PerpError("INVALID_PARAMS", `Arcus only withdraws to the account owner ${owner}`, {
1078
+ remediation: `Omit --to (defaults to ${owner})`,
1079
+ });
1088
1080
  }
1081
+ const destination = owner;
1089
1082
  const plan = { venue: def.id, network: net, asset, amount: String(amountNum), destination, chain };
1090
1083
  if (isDryRun()) {
1091
1084
  logExecution({
@@ -1100,16 +1093,6 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
1100
1093
  console.log(chalk.gray(" Nothing was sent.\n"));
1101
1094
  return;
1102
1095
  }
1103
- if (venueId === "ondo") {
1104
- // Address-book preflight (read-only): Ondo refuses destinations that are not listed.
1105
- const { OndoAdapter } = await import("../exchanges/ondo/adapter.js");
1106
- if (!(adapter instanceof OndoAdapter))
1107
- throw new Error("ondo venue did not produce an OndoAdapter");
1108
- const book = await adapter.addressBook();
1109
- if (!book.some(a => a.toLowerCase() === destination.toLowerCase())) {
1110
- throw (await import("../exchanges/ondo/funds.js")).ondoAddressNotOnBook(destination, book);
1111
- }
1112
- }
1113
1096
  if (!isJson()) {
1114
1097
  console.log(chalk.cyan(`\n Withdrawing ${formatUsd(amountNum)} ${asset} from ${def.displayName}...\n`));
1115
1098
  console.log(` Destination: ${destination} (${chain})`);
@@ -1139,7 +1122,7 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
1139
1122
  });
1140
1123
  }
1141
1124
  // ── QFEX (design §14.6, §14.12 #8) — custodial; deposit = address display, withdraw = banker POST
1142
- // gated by settings.withdraw.qfex inside QfexAdapter.withdraw(). ──
1125
+ // gated by settings.withdraw.qfex inside QfexAdapter.withdraw(); switched off (QFEX_WITHDRAW_ENABLED). ──
1143
1126
  /** QFEX adapter for the active network — the default imported key, or the one named by --agent. */
1144
1127
  const qfexAdapterFor = async (agentName) => {
1145
1128
  const { QfexAdapter: Cls } = await import("../exchanges/qfex/adapter.js");
@@ -1164,11 +1147,15 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
1164
1147
  throw new Error("qfex venue did not produce a QfexAdapter");
1165
1148
  return adapter;
1166
1149
  };
1167
- supportsDryRun(withdraw.command("qfex <amount>"))
1168
- .description("Withdraw USDC from QFEX to an allowlisted Arbitrum One address (needs settings withdraw.qfex and a key with the \"Deposit and withdraw\" permission; supports --dry-run)")
1150
+ supportsDryRun(withdraw.command(QFEX_WITHDRAW_ENABLED ? "qfex <amount>" : "qfex [amount]"))
1151
+ .description(QFEX_WITHDRAW_ENABLED
1152
+ ? "Withdraw USDC from QFEX to an allowlisted Arbitrum One address (needs settings withdraw.qfex and a key with the \"Deposit and withdraw\" permission; supports --dry-run)"
1153
+ : "Not available: QFEX withdrawals are disabled in perp-cli for now — use the QFEX web app (refused, also with --dry-run)")
1169
1154
  .option("--to <address>", "Destination EVM address on Arbitrum One (required; must be on the settings withdraw.qfex allowlist)")
1170
1155
  .option("--agent <name>", "Imported QFEX key to use (a key with the \"Deposit and withdraw\" permission, bound to the primary account)")
1171
1156
  .action(async (amount, opts) => {
1157
+ if (!QFEX_WITHDRAW_ENABLED)
1158
+ throw unsupported("QFEX", "withdraw", QFEX_WITHDRAW_DISABLED.reason, QFEX_WITHDRAW_DISABLED.remediation);
1172
1159
  const def = getVenue("qfex");
1173
1160
  const net = getNetwork();
1174
1161
  if (!/^\d+(\.\d+)?$/.test(amount) || !(Number(amount) > 0)) {
@@ -1247,7 +1234,7 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
1247
1234
  console.log(chalk.yellow(`\n ${info.warning}`));
1248
1235
  console.log(chalk.gray(" perp-cli did not send anything — transfer from your own wallet or exchange.\n"));
1249
1236
  });
1250
- // ── Aster deposit / withdraw, Ondo deposit, Ondo address book (OWS-master on-chain legs) ──
1237
+ // ── Aster deposit / withdraw, Ondo deposit (OWS-master on-chain legs; address book refused) ──
1251
1238
  registerOnchainFundsCommands({
1252
1239
  program, funds, deposit, withdraw, isJson, isDryRun, getNetwork, getAdapterForExchange,
1253
1240
  requireMaster, printFundsPlan, failStatus: fundsFailStatus,
@@ -1361,8 +1348,8 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
1361
1348
  hyperliquid: { chain: "Arbitrum", token: "USDC", speed: "minutes", fee: "$1" },
1362
1349
  lighter: { chain: "Ethereum L1", token: "USDC", speed: "~12 hours (standard), minutes (fast)", fee: "varies" },
1363
1350
  "lighter-rh": { chain: "Robinhood Chain", token: "USDG", command: "perp -e lighter-rh funds withdraw lighter <amount>" },
1364
- ondo: { chain: "Ethereum", token: "USDC", command: "perp funds withdraw ondo <amount> --to <address>", addressBook: "destination must be listed: perp funds address-book add ondo <master address>" },
1365
- qfex: { chain: "Arbitrum", token: "USDC", command: "perp funds withdraw qfex <amount> --to <allowlisted address>", gate: "settings withdraw.qfex (allowlist, maxPerTx, maxPerDay)", note: "needs a key with the \"Deposit and withdraw\" permission (All accounts access)" },
1351
+ ondo: { supported: false, note: "the Ondo Perps API does not allow withdrawals — withdraw in the Ondo Perps web app" },
1352
+ qfex: { supported: false, note: "QFEX withdrawals are disabled in perp-cli for now — withdraw in the QFEX web app" },
1366
1353
  arcus: { chain: "Robinhood Chain", token: "USDG", command: "perp funds withdraw arcus <amount>" },
1367
1354
  aster: { chain: "BNB Chain (default) / Arbitrum", token: "USDT (default) / USDC", command: "perp funds withdraw aster <amount> [--chain bsc|arbitrum] [--asset USDT|USDC]", fee: "POST /fapi/v3/aster/user-withdraw-info (taken from the amount)", requires: "mainnet; API wallet approved with --can-withdraw --ip-whitelist; destination = the account's own address" },
1368
1355
  },
@@ -1411,9 +1398,8 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
1411
1398
  console.log(` Command: ${chalk.green("perp -e lt funds withdraw lighter <amount>")}`);
1412
1399
  console.log(chalk.white.bold("\n Lighter RH") + chalk.gray(" → Robinhood Chain (USDG)"));
1413
1400
  console.log(` Command: ${chalk.green("perp -e lighter-rh funds withdraw lighter <amount>")}`);
1414
- console.log(chalk.white.bold("\n Ondo Perps") + chalk.gray(" → Ethereum (USDC)"));
1415
- console.log(` Command: ${chalk.green("perp funds withdraw ondo <amount> --to <address>")}`);
1416
- console.log(` Address book: ${chalk.green("perp funds address-book add ondo <your master address>")}`);
1401
+ console.log(chalk.white.bold("\n Ondo Perps") + chalk.gray(" — not available over the API"));
1402
+ console.log(chalk.gray(" Withdraw in the Ondo Perps web app."));
1417
1403
  console.log(chalk.white.bold("\n Arcus") + chalk.gray(" → Robinhood Chain (USDG, to the account owner)"));
1418
1404
  console.log(` Command: ${chalk.green("perp funds withdraw arcus <amount>")}`);
1419
1405
  console.log(chalk.white.bold("\n Aster") + chalk.gray(" → BNB Chain / Arbitrum (USDT, to the account's own address; mainnet only)"));
@@ -647,24 +647,42 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
647
647
  let totalEquity = 0;
648
648
  let totalUPnl = 0;
649
649
  const symbolSet = new Set();
650
+ // A venue whose read failed is reported here and left out of the totals — never counted as 0.
651
+ const errors = {};
652
+ const errText = (e) => (e instanceof Error ? e.message : String(e));
650
653
  await Promise.allSettled(exchanges.map(async (ex) => {
654
+ let adapter;
651
655
  try {
652
- const adapter = await getAdapterForExchange(ex);
653
- const [balance, trades, funding] = await Promise.all([
654
- adapter.getBalance(),
655
- adapter.getTradeHistory(50).catch(() => []),
656
- adapter.getFundingPayments(50).catch(() => []),
657
- ]);
656
+ adapter = await getAdapterForExchange(ex);
657
+ }
658
+ catch (e) {
659
+ errors[`${ex}.adapter`] = errText(e);
660
+ return;
661
+ }
662
+ try {
663
+ const balance = await adapter.getBalance();
658
664
  totalEquity += Number(balance.equity);
659
665
  totalUPnl += Number(balance.unrealizedPnl);
660
- for (const t of trades) {
666
+ }
667
+ catch (e) {
668
+ errors[`${ex}.balance`] = errText(e);
669
+ return;
670
+ }
671
+ const [trades, funding] = await Promise.allSettled([adapter.getTradeHistory(50), adapter.getFundingPayments(50)]);
672
+ if (trades.status === "fulfilled") {
673
+ for (const t of trades.value) {
661
674
  totalFees += Number(t.fee);
662
675
  symbolSet.add(t.symbol);
663
676
  }
664
- for (const f of funding)
677
+ }
678
+ else
679
+ errors[`${ex}.trades`] = errText(trades.reason);
680
+ if (funding.status === "fulfilled") {
681
+ for (const f of funding.value)
665
682
  totalFunding += Number(f.payment);
666
683
  }
667
- catch { /* skip */ }
684
+ else
685
+ errors[`${ex}.funding`] = errText(funding.reason);
668
686
  }));
669
687
  const report = {
670
688
  period: opts.since ?? "all time",
@@ -691,6 +709,7 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
691
709
  ? (stats.totalTrades / Math.max(1, (Date.now() - new Date(records[records.length - 1]?.timestamp ?? Date.now()).getTime()) / 86400000)).toFixed(1)
692
710
  : "0",
693
711
  },
712
+ errors,
694
713
  };
695
714
  if (isJson())
696
715
  return printJson(jsonOk(report));
@@ -707,6 +726,8 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
707
726
  console.log(` Trading Fees: ${chalk.red(`-$${formatUsd(totalFees)}`)}`);
708
727
  console.log(` Funding Income: ${formatPnl(totalFunding)}`);
709
728
  console.log(` Net: ${formatPnl(totalFunding - totalFees)}`);
729
+ for (const [k, msg] of Object.entries(errors))
730
+ console.log(chalk.yellow(` ⚠ ${k} unavailable (left out of the totals): ${msg}`));
710
731
  console.log();
711
732
  });
712
733
  });