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
|
@@ -11,11 +11,11 @@ import { computeMatchedSize, computeSpotPerpMatchedSize } from "../arb/index.js"
|
|
|
11
11
|
import { toHourlyRate } from "../funding.js";
|
|
12
12
|
import { toHourlyRateByBasis } from "../funding/normalize.js";
|
|
13
13
|
import { fetchPacificaPricesRaw, parsePacificaRaw, fetchHyperliquidMetaRaw, parseHyperliquidMetaRaw, fetchLighterOrderBookDetailsRaw, fetchLighterFundingRatesRaw, parseLighterRaw, } from "../shared-api.js";
|
|
14
|
-
import {
|
|
14
|
+
import { fetchAllDexAssetsDetailed, findDexArbPairs, scanDexArb } from "../dex-asset-map.js";
|
|
15
15
|
import { logExecution, readExecutionLog } from "../execution-log.js";
|
|
16
16
|
import { smartOrder } from "../smart-order.js";
|
|
17
17
|
import { aggressiveSettleBoost, estimateFundingUntilSettlement, computeBasisRisk, notifyIfEnabled, } from "../arb-utils.js";
|
|
18
|
-
import { checkChainMargins,
|
|
18
|
+
import { checkChainMargins, marginEntryBlocks, computeAutoSize, } from "../cross-chain-margin.js";
|
|
19
19
|
import { loadArbState, addPosition as persistAddPosition, removePosition as persistRemovePosition, updatePosition as persistUpdatePosition, createInitialState, createDaemonStateStore, } from "../arb-state.js";
|
|
20
20
|
import { handleRates, handleBasisScan, handleDexScan, handleCompare, handleFundingHistory, handleFundingPositions, fetchAllPrices, printGapTable, printTrackSummary, formatGapPrice, } from "./arb.js";
|
|
21
21
|
// ── Fee-Adjusted Net Spread Calculation ──
|
|
@@ -323,6 +323,62 @@ export function isSpreadReversed(longExchange, shortExchange, snapshot) {
|
|
|
323
323
|
// Reversed if the long side rate exceeds the short side rate
|
|
324
324
|
return longHourly > shortHourly;
|
|
325
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Read open positions on each venue for `arb pnl`. A venue whose adapter/positions read
|
|
328
|
+
* fails lands in `positionErrors` (never silently dropped); a venue with no credentials
|
|
329
|
+
* (read-only adapter) lands in `notConfigured`.
|
|
330
|
+
*/
|
|
331
|
+
export async function collectArbPnlPositions(exchangeNames, getAdapterForExchange) {
|
|
332
|
+
const positions = [];
|
|
333
|
+
const positionErrors = {};
|
|
334
|
+
const notConfigured = [];
|
|
335
|
+
for (const exName of exchangeNames) {
|
|
336
|
+
try {
|
|
337
|
+
const adapter = await getAdapterForExchange(exName);
|
|
338
|
+
if (adapter.isReadOnly === true) {
|
|
339
|
+
notConfigured.push(exName);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
for (const p of await adapter.getPositions()) {
|
|
343
|
+
positions.push({
|
|
344
|
+
exchange: exName,
|
|
345
|
+
symbol: p.symbol.replace("-PERP", ""),
|
|
346
|
+
side: p.side,
|
|
347
|
+
size: Math.abs(Number(p.size)),
|
|
348
|
+
entry: Number(p.entryPrice),
|
|
349
|
+
mark: Number(p.markPrice),
|
|
350
|
+
upnl: Number(p.unrealizedPnl),
|
|
351
|
+
lev: p.leverage,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch (e) {
|
|
356
|
+
positionErrors[exName] = e instanceof Error ? e.message : String(e);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return { positions, positionErrors, notConfigured };
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* "arb" when the symbol has both a long and a short leg; otherwise "unknown" if any venue's
|
|
363
|
+
* positions were unread (its missing leg may be there — never call it naked), else "single".
|
|
364
|
+
*/
|
|
365
|
+
export function arbPnlPairStatus(positions, failedVenues) {
|
|
366
|
+
if (positions.length >= 2 && positions.some(p => p.side === "long") && positions.some(p => p.side === "short"))
|
|
367
|
+
return "arb";
|
|
368
|
+
return failedVenues.length > 0 ? "unknown" : "single";
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* HIP-3 auto-arb: the dex (either leg) whose market read failed this cycle, or null.
|
|
372
|
+
* A non-null result means the pair's spread is UNKNOWN — the position is kept (never
|
|
373
|
+
* closed as "pair disappeared"); only a pair missing from a successfully read dex is gone.
|
|
374
|
+
*/
|
|
375
|
+
export function dexCloseBlockedBy(pos, failedDexes) {
|
|
376
|
+
if (Object.hasOwn(failedDexes, pos.longDex))
|
|
377
|
+
return pos.longDex;
|
|
378
|
+
if (Object.hasOwn(failedDexes, pos.shortDex))
|
|
379
|
+
return pos.shortDex;
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
326
382
|
const _warnedArbPriceMismatch = new Set();
|
|
327
383
|
async function fetchFundingSpreads() {
|
|
328
384
|
// Selected network (review L7): the Pacifica / HL / Lighter raw fetchers follow -n via
|
|
@@ -587,6 +643,8 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
587
643
|
const openPositions = [];
|
|
588
644
|
// Track which exchanges have low margin (block entries)
|
|
589
645
|
const blockedExchanges = new Set();
|
|
646
|
+
// Venues whose margin was unknown last cycle (webhook sent once per transition)
|
|
647
|
+
const marginUnknownNotified = new Set();
|
|
590
648
|
// Track close times per symbol to prevent close→re-entry loops
|
|
591
649
|
const closeCooldowns = new Map(); // symbol → close timestamp ms
|
|
592
650
|
// Consecutive definite close failures per symbol (quarantined at MAX_PAIR_CLOSE_FAILURES)
|
|
@@ -960,40 +1018,74 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
960
1018
|
console.log(chalk.yellow(` ${now} DEGRADED ${pos.symbol}: ${downExchanges.has(pos.longExchange) ? pos.longExchange : pos.shortExchange} is down`));
|
|
961
1019
|
}
|
|
962
1020
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1021
|
+
// Only check margins when we have open positions (avoid slow adapter init).
|
|
1022
|
+
// An unreadable margin is UNKNOWN, never healthy: logged every cycle (stderr) and new
|
|
1023
|
+
// entries on that venue are blocked (closing / reducing stays allowed).
|
|
1024
|
+
if (openPositions.length > 0) {
|
|
1025
|
+
const marginCheck = { statuses: [], unknown: [] };
|
|
1026
|
+
try {
|
|
1027
|
+
const adaptersMap = new Map();
|
|
1028
|
+
for (const name of ["hyperliquid", "lighter", "pacifica"]) {
|
|
970
1029
|
try {
|
|
971
|
-
|
|
1030
|
+
const a = await getAdapterForExchange(name);
|
|
1031
|
+
// No credentials (read-only adapter): no positions / margin there to monitor, and
|
|
1032
|
+
// the bot cannot enter on it either — not "unknown".
|
|
1033
|
+
if (a.isReadOnly === true) {
|
|
1034
|
+
blockedExchanges.add(name);
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
adaptersMap.set(name, a);
|
|
972
1038
|
}
|
|
973
|
-
catch {
|
|
1039
|
+
catch (e) {
|
|
1040
|
+
marginCheck.unknown.push({ exchange: name, error: `adapter init failed: ${e instanceof Error ? e.message : String(e)}` });
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (adaptersMap.size > 0) {
|
|
1044
|
+
const res = await checkChainMargins(adaptersMap, minMarginPct);
|
|
1045
|
+
marginCheck.statuses.push(...res.statuses);
|
|
1046
|
+
marginCheck.unknown.push(...res.unknown);
|
|
974
1047
|
}
|
|
975
1048
|
}
|
|
976
|
-
|
|
977
|
-
const
|
|
978
|
-
for (const
|
|
979
|
-
if (
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1049
|
+
catch (e) {
|
|
1050
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1051
|
+
for (const name of ["hyperliquid", "lighter", "pacifica"]) {
|
|
1052
|
+
if (blockedExchanges.has(name))
|
|
1053
|
+
continue;
|
|
1054
|
+
if (!marginCheck.statuses.some(s => s.exchange === name) && !marginCheck.unknown.some(u => u.exchange === name))
|
|
1055
|
+
marginCheck.unknown.push({ exchange: name, error: msg });
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
const nowUnknown = new Set();
|
|
1059
|
+
for (const b of marginEntryBlocks(marginCheck, minMarginPct)) {
|
|
1060
|
+
blockedExchanges.add(b.exchange);
|
|
1061
|
+
if (b.level === "critical") {
|
|
1062
|
+
console.log(chalk.red.bold(` ${now} EMERGENCY: ${b.exchange} margin ratio ${b.marginPct.toFixed(1)}% — CRITICAL (below 15%)`));
|
|
1063
|
+
await notifyIfEnabled(webhookUrl, notifyEvents, "margin", {
|
|
1064
|
+
exchange: b.exchange, marginPct: b.marginPct, threshold: 15,
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
else if (b.level === "low") {
|
|
1068
|
+
console.log(chalk.yellow(` ${now} WARNING: ${b.exchange} margin ${b.marginPct.toFixed(1)}% below ${minMarginPct}% — blocking new entries`));
|
|
1069
|
+
await notifyIfEnabled(webhookUrl, notifyEvents, "margin", {
|
|
1070
|
+
exchange: b.exchange, marginPct: b.marginPct, threshold: minMarginPct,
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
else {
|
|
1074
|
+
nowUnknown.add(b.exchange);
|
|
1075
|
+
console.error(chalk.yellow(` ${now} MARGIN UNKNOWN: ${b.exchange} (${b.error}) — blocking new entries until margin is readable`));
|
|
1076
|
+
// Webhook once per transition into unknown (stderr carries the per-cycle signal).
|
|
1077
|
+
if (!marginUnknownNotified.has(b.exchange)) {
|
|
989
1078
|
await notifyIfEnabled(webhookUrl, notifyEvents, "margin", {
|
|
990
|
-
exchange:
|
|
1079
|
+
exchange: b.exchange, marginPct: null, threshold: minMarginPct,
|
|
1080
|
+
message: `MARGIN UNKNOWN: ${b.exchange} (${b.error}) — new entries blocked`,
|
|
991
1081
|
});
|
|
992
1082
|
}
|
|
993
1083
|
}
|
|
994
1084
|
}
|
|
1085
|
+
marginUnknownNotified.clear();
|
|
1086
|
+
for (const ex of nowUnknown)
|
|
1087
|
+
marginUnknownNotified.add(ex);
|
|
995
1088
|
}
|
|
996
|
-
catch { /* margin check failed, continue without blocking */ }
|
|
997
1089
|
// Entry decisions: every monitored symbol that is not entered gets its reason — logged per
|
|
998
1090
|
// symbol for --symbols, summarized per cycle for a full scan (a silent run is impossible).
|
|
999
1091
|
const skipCounts = new Map();
|
|
@@ -1058,7 +1150,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1058
1150
|
const shortExchange = snap.shortExch;
|
|
1059
1151
|
// Block entries if either exchange has low margin
|
|
1060
1152
|
if (blockedExchanges.has(longExchange) || blockedExchanges.has(shortExchange)) {
|
|
1061
|
-
skip(snap.symbol, `entries blocked on ${blockedExchanges.has(longExchange) ? longExchange : shortExchange} (margin low / venue down)`);
|
|
1153
|
+
skip(snap.symbol, `entries blocked on ${blockedExchanges.has(longExchange) ? longExchange : shortExchange} (margin low / margin unknown / venue down)`);
|
|
1062
1154
|
continue;
|
|
1063
1155
|
}
|
|
1064
1156
|
// Compute net spread after fees and bridge costs
|
|
@@ -2624,30 +2716,22 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
2624
2716
|
if (!isJson())
|
|
2625
2717
|
console.log(chalk.cyan("\n Checking arb positions...\n"));
|
|
2626
2718
|
const exchangeNames = ["hyperliquid", "lighter", "pacifica"];
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
side: p.side,
|
|
2637
|
-
size: Math.abs(Number(p.size)),
|
|
2638
|
-
entry: Number(p.entryPrice),
|
|
2639
|
-
mark: Number(p.markPrice),
|
|
2640
|
-
upnl: Number(p.unrealizedPnl),
|
|
2641
|
-
lev: p.leverage,
|
|
2642
|
-
});
|
|
2643
|
-
}
|
|
2644
|
-
}
|
|
2645
|
-
catch {
|
|
2646
|
-
// exchange not configured, skip
|
|
2647
|
-
}
|
|
2719
|
+
// A venue whose positions could not be read is reported (never dropped): pairs that may
|
|
2720
|
+
// involve it are UNKNOWN, not naked. Only a venue with no credentials is skipped — and said so.
|
|
2721
|
+
const { positions: allPositions, positionErrors, notConfigured } = await collectArbPnlPositions(exchangeNames, getAdapterForExchange);
|
|
2722
|
+
const failedVenues = Object.keys(positionErrors);
|
|
2723
|
+
if (!isJson()) {
|
|
2724
|
+
for (const ex of notConfigured)
|
|
2725
|
+
console.log(chalk.gray(` ${ex}: not configured (no credentials) — skipped`));
|
|
2726
|
+
for (const [ex, msg] of Object.entries(positionErrors))
|
|
2727
|
+
console.error(chalk.red(` ${ex}: positions unavailable — ${msg} (pairs involving ${ex} are UNKNOWN)`));
|
|
2648
2728
|
}
|
|
2649
2729
|
if (allPositions.length === 0) {
|
|
2650
|
-
|
|
2730
|
+
if (isJson())
|
|
2731
|
+
return printJson(jsonOk({ pairs: [], positionErrors, notConfigured }));
|
|
2732
|
+
console.log(failedVenues.length > 0
|
|
2733
|
+
? chalk.yellow(` No positions read — ${failedVenues.join(", ")} unavailable, holdings there are UNKNOWN.\n`)
|
|
2734
|
+
: chalk.gray(" No positions found on any configured exchange.\n"));
|
|
2651
2735
|
return;
|
|
2652
2736
|
}
|
|
2653
2737
|
// Group by symbol to find arb pairs
|
|
@@ -2665,6 +2749,8 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
2665
2749
|
const TAKER_FEE = getTakerFee("default");
|
|
2666
2750
|
// Fetch actual settled funding payments from each exchange
|
|
2667
2751
|
const actualFundingByExSymbol = new Map(); // "exchange:SYMBOL" → total settled USD
|
|
2752
|
+
// Venues whose funding history could not be read: their symbols' actualFunding is null, never 0.
|
|
2753
|
+
const fundingErrors = {};
|
|
2668
2754
|
for (const exName of exchangeNames) {
|
|
2669
2755
|
try {
|
|
2670
2756
|
const adapter = await getAdapterForExchange(exName);
|
|
@@ -2675,30 +2761,39 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
2675
2761
|
actualFundingByExSymbol.set(key, (actualFundingByExSymbol.get(key) ?? 0) + Number(fp.payment));
|
|
2676
2762
|
}
|
|
2677
2763
|
}
|
|
2678
|
-
catch {
|
|
2679
|
-
|
|
2764
|
+
catch (e) {
|
|
2765
|
+
fundingErrors[exName] = e instanceof Error ? e.message : String(e);
|
|
2680
2766
|
}
|
|
2681
2767
|
}
|
|
2768
|
+
/** Settled funding of `symbol` across its legs; null when a leg's venue history was not read. */
|
|
2769
|
+
const actualFundingOf = (symbol, positions) => {
|
|
2770
|
+
let sum = 0;
|
|
2771
|
+
for (const p of positions) {
|
|
2772
|
+
if (fundingErrors[p.exchange] !== undefined)
|
|
2773
|
+
return null;
|
|
2774
|
+
sum += actualFundingByExSymbol.get(`${p.exchange}:${symbol}`) ?? 0; // read, no payment for this symbol = 0
|
|
2775
|
+
}
|
|
2776
|
+
return sum;
|
|
2777
|
+
};
|
|
2682
2778
|
if (isJson()) {
|
|
2683
2779
|
const result = [...bySymbol.entries()].map(([symbol, positions]) => {
|
|
2684
2780
|
const spread = spreadMap.get(symbol);
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
for (const p of positions) {
|
|
2688
|
-
actualFunding += actualFundingByExSymbol.get(`${p.exchange}:${symbol}`) ?? 0;
|
|
2689
|
-
}
|
|
2690
|
-
return { symbol, positions, currentSpread: spread?.spread ?? 0, actualFunding };
|
|
2781
|
+
const failed = positions.map(p => fundingErrors[p.exchange] === undefined ? null : `${p.exchange}: ${fundingErrors[p.exchange]}`).filter((x) => x !== null);
|
|
2782
|
+
return { symbol, positions, pairStatus: arbPnlPairStatus(positions, failedVenues), currentSpread: spread === undefined ? null : spread.spread, actualFunding: actualFundingOf(symbol, positions), fundingError: failed.length > 0 ? failed.join("; ") : null };
|
|
2691
2783
|
});
|
|
2692
|
-
return printJson(jsonOk(result));
|
|
2784
|
+
return printJson(jsonOk({ pairs: result, positionErrors, notConfigured }));
|
|
2693
2785
|
}
|
|
2694
2786
|
let totalUpnl = 0;
|
|
2695
2787
|
let totalFees = 0;
|
|
2696
2788
|
let totalEstFunding = 0;
|
|
2697
2789
|
let totalActualFunding = 0;
|
|
2790
|
+
let actualFundingIncomplete = false;
|
|
2698
2791
|
for (const [symbol, positions] of bySymbol) {
|
|
2699
|
-
const
|
|
2792
|
+
const pairStatus = arbPnlPairStatus(positions, failedVenues);
|
|
2700
2793
|
const spread = spreadMap.get(symbol);
|
|
2701
|
-
console.log(chalk.white.bold(` ${symbol}`) + (
|
|
2794
|
+
console.log(chalk.white.bold(` ${symbol}`) + (pairStatus === "arb" ? chalk.green(" [ARB PAIR]")
|
|
2795
|
+
: pairStatus === "unknown" ? chalk.yellow(` [UNKNOWN — ${failedVenues.join(", ")} positions unread]`)
|
|
2796
|
+
: chalk.gray(" [single]")));
|
|
2702
2797
|
for (const p of positions) {
|
|
2703
2798
|
const sideColor = p.side === "long" ? chalk.green : chalk.red;
|
|
2704
2799
|
const pnlColor = p.upnl >= 0 ? chalk.green : chalk.red;
|
|
@@ -2734,19 +2829,22 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
2734
2829
|
const holdHours = entryTime ? (Date.now() - entryTime) / (1000 * 60 * 60) : null;
|
|
2735
2830
|
// Estimated funding based on current spread × hold time
|
|
2736
2831
|
const estFunding = holdHours !== null ? hourlyIncome * holdHours : 0;
|
|
2737
|
-
// Actual settled funding from exchange APIs
|
|
2738
|
-
|
|
2739
|
-
for (const p of positions) {
|
|
2740
|
-
actualFunding += actualFundingByExSymbol.get(`${p.exchange}:${symbol}`) ?? 0;
|
|
2741
|
-
}
|
|
2832
|
+
// Actual settled funding from exchange APIs (null = a leg's venue history was not read)
|
|
2833
|
+
const actualFunding = actualFundingOf(symbol, positions);
|
|
2742
2834
|
totalEstFunding += estFunding;
|
|
2743
|
-
totalActualFunding += actualFunding;
|
|
2744
|
-
const diff = actualFunding - estFunding;
|
|
2745
|
-
const diffColor = Math.abs(diff) < 0.01 ? chalk.gray : diff >= 0 ? chalk.green : chalk.red;
|
|
2746
2835
|
const fmtVal = (v) => v >= 0 ? `$${v.toFixed(4)}` : `-$${Math.abs(v).toFixed(4)}`;
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
`
|
|
2836
|
+
if (actualFunding === null) {
|
|
2837
|
+
actualFundingIncomplete = true;
|
|
2838
|
+
console.log(chalk.white(` Funding: Est. ${chalk.yellow(fmtVal(estFunding))} / Actual ${chalk.yellow("unavailable")}`));
|
|
2839
|
+
}
|
|
2840
|
+
else {
|
|
2841
|
+
totalActualFunding += actualFunding;
|
|
2842
|
+
const diff = actualFunding - estFunding;
|
|
2843
|
+
const diffColor = Math.abs(diff) < 0.01 ? chalk.gray : diff >= 0 ? chalk.green : chalk.red;
|
|
2844
|
+
console.log(chalk.white(` Funding: Est. ${chalk.yellow(fmtVal(estFunding))} / ` +
|
|
2845
|
+
`Actual ${chalk.cyan(fmtVal(actualFunding))} / ` +
|
|
2846
|
+
`Diff: ${diffColor(fmtVal(diff))}`));
|
|
2847
|
+
}
|
|
2750
2848
|
}
|
|
2751
2849
|
}
|
|
2752
2850
|
console.log();
|
|
@@ -2764,10 +2862,12 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
2764
2862
|
const fundingDiff = totalActualFunding - totalEstFunding;
|
|
2765
2863
|
const diffPct = totalEstFunding !== 0 ? ((fundingDiff / Math.abs(totalEstFunding)) * 100).toFixed(1) : "N/A";
|
|
2766
2864
|
console.log(` Est. funding: ${chalk.yellow(`$${totalEstFunding.toFixed(4)}`)}`);
|
|
2767
|
-
console.log(` Actual funding: ${chalk.cyan(`$${totalActualFunding.toFixed(4)}`)}`);
|
|
2865
|
+
console.log(` Actual funding: ${chalk.cyan(`$${totalActualFunding.toFixed(4)}`)}${actualFundingIncomplete ? chalk.yellow(" (incomplete: some venues' funding history unavailable)") : ""}`);
|
|
2768
2866
|
console.log(` Funding diff: ${fundingDiff >= 0 ? chalk.green(`+$${fundingDiff.toFixed(4)}`) : chalk.red(`-$${Math.abs(fundingDiff).toFixed(4)}`)} (${diffPct}%)`);
|
|
2769
2867
|
}
|
|
2770
|
-
console.log(` Net (if closed now): ${netColor(`$${netPnl.toFixed(4)}`)}`);
|
|
2868
|
+
console.log(` Net (if closed now): ${netColor(`$${netPnl.toFixed(4)}`)}${actualFundingIncomplete ? chalk.yellow(" (excludes unread funding)") : ""}`);
|
|
2869
|
+
for (const [ex, msg] of Object.entries(fundingErrors))
|
|
2870
|
+
console.log(chalk.yellow(` ${ex} funding history unavailable: ${msg}`));
|
|
2771
2871
|
console.log(chalk.gray(` (Fees assume ${(TAKER_FEE * 100).toFixed(3)}% taker. Actual may vary.)`));
|
|
2772
2872
|
console.log(chalk.gray(` * Net includes actual settled funding where available.\n`));
|
|
2773
2873
|
}
|
|
@@ -2787,10 +2887,13 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
2787
2887
|
cycle++;
|
|
2788
2888
|
const ts = new Date().toLocaleTimeString();
|
|
2789
2889
|
try {
|
|
2790
|
-
const pairs = await scanDexArb({
|
|
2890
|
+
const { pairs, failedDexes } = await scanDexArb({
|
|
2791
2891
|
minAnnualSpread: minSpread,
|
|
2792
2892
|
includeNative: opts.includeNative,
|
|
2793
2893
|
});
|
|
2894
|
+
// Failed dex reads are reported, never shown as "no opportunities" for that dex.
|
|
2895
|
+
for (const [dex, msg] of Object.entries(failedDexes))
|
|
2896
|
+
console.error(chalk.yellow(` ${ts} HIP-3 dex ${dex} unavailable: ${msg} — its pairs are missing this cycle`));
|
|
2794
2897
|
const shown = pairs.slice(0, topN);
|
|
2795
2898
|
// Clear previous output
|
|
2796
2899
|
if (cycle > 1) {
|
|
@@ -2859,7 +2962,22 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
2859
2962
|
// Positions live only in memory: a restart re-reads what is held on the dexes, so a
|
|
2860
2963
|
// held underlying is managed (closed when the spread converges), never re-entered.
|
|
2861
2964
|
// Unpaired / size-mismatched legs are quarantined. An unreadable dex → not started.
|
|
2862
|
-
|
|
2965
|
+
// A transient 429 / timeout on one of the deployed dexes is retried (2 s, 5 s) before
|
|
2966
|
+
// refusing: a partial read is never cached, so each attempt re-reads every dex.
|
|
2967
|
+
let { assets, failedDexes: startFailed } = await fetchAllDexAssetsDetailed();
|
|
2968
|
+
for (const waitMs of [2_000, 5_000]) {
|
|
2969
|
+
if (Object.keys(startFailed).length === 0)
|
|
2970
|
+
break;
|
|
2971
|
+
console.error(chalk.yellow(` HIP-3 dex read failed at start (${Object.keys(startFailed).join(", ")}) — retrying in ${waitMs / 1000}s`));
|
|
2972
|
+
await new Promise(r => setTimeout(r, waitMs));
|
|
2973
|
+
({ assets, failedDexes: startFailed } = await fetchAllDexAssetsDetailed());
|
|
2974
|
+
}
|
|
2975
|
+
if (Object.keys(startFailed).length > 0) {
|
|
2976
|
+
// A dex not read → its held legs are unknown: not started (never re-enter a held pair).
|
|
2977
|
+
throw new PerpError("EXCHANGE_ERROR", `arb auto --hip3: dex market data unreadable at start (${Object.entries(startFailed).map(([d, m]) => `${d}: ${m}`).join("; ")}) — not started (held pairs unknown)`, {
|
|
2978
|
+
exchange: "hyperliquid", remediation: `Check connectivity (perp arb scan --hip3), then restart`,
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2863
2981
|
const pairsNow = findDexArbPairs(assets, { minAnnualSpread: 0, includeNative: true });
|
|
2864
2982
|
const legs = new Map();
|
|
2865
2983
|
for (const dex of [...new Set(assets.map(a => a.dex))]) {
|
|
@@ -2931,16 +3049,24 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
2931
3049
|
}
|
|
2932
3050
|
const cycle = async () => {
|
|
2933
3051
|
try {
|
|
2934
|
-
const pairs = await scanDexArb({
|
|
3052
|
+
const { pairs, failedDexes } = await scanDexArb({
|
|
2935
3053
|
minAnnualSpread: 0, // get all, filter ourselves
|
|
2936
3054
|
includeNative: true,
|
|
2937
3055
|
});
|
|
2938
3056
|
const now = new Date().toLocaleTimeString();
|
|
3057
|
+
for (const [dex, msg] of Object.entries(failedDexes))
|
|
3058
|
+
console.error(chalk.yellow(` ${now} HIP-3 dex ${dex} unavailable: ${msg}`));
|
|
2939
3059
|
// Check close conditions
|
|
2940
3060
|
for (let i = openPositions.length - 1; i >= 0; i--) {
|
|
2941
3061
|
if (stop.requested())
|
|
2942
3062
|
break; // stopping: no new close sequence
|
|
2943
3063
|
const pos = openPositions[i];
|
|
3064
|
+
// A leg's dex not read this cycle → spread unknown: never close on missing data.
|
|
3065
|
+
const unreadDex = dexCloseBlockedBy(pos, failedDexes);
|
|
3066
|
+
if (unreadDex !== null) {
|
|
3067
|
+
console.error(chalk.yellow(` ${now} dex ${unreadDex} unavailable — ${pos.underlying} position kept, re-checked next cycle`));
|
|
3068
|
+
continue;
|
|
3069
|
+
}
|
|
2944
3070
|
// Find current pair for this position
|
|
2945
3071
|
const current = pairs.find(p => p.underlying === pos.underlying &&
|
|
2946
3072
|
((p.long.dex === pos.longDex && p.short.dex === pos.shortDex) ||
|
package/dist/commands/arb.js
CHANGED
|
@@ -28,34 +28,33 @@ async function fetchHyperliquidRates() {
|
|
|
28
28
|
async function fetchAsterRates() {
|
|
29
29
|
// SSOT rule #2: do NOT silently coerce missing markPrice / lastFundingRate
|
|
30
30
|
// to 0 — that would corrupt comparisons against other DEXs. Skip symbols
|
|
31
|
-
// whose Aster row is malformed instead.
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
symbol: rawSym.replace(/USDT$/, ""),
|
|
50
|
-
fundingRate,
|
|
51
|
-
markPrice,
|
|
52
|
-
});
|
|
31
|
+
// whose Aster row is malformed instead. An endpoint failure PROPAGATES: the
|
|
32
|
+
// caller (handleBasisScan) runs every DEX under Promise.allSettled and reports
|
|
33
|
+
// the rejection on stderr — Aster is never silently absent from the scan.
|
|
34
|
+
const res = await fetch("https://fapi.asterdex.com/fapi/v1/premiumIndex");
|
|
35
|
+
if (!res.ok)
|
|
36
|
+
throw new Error(`Aster premiumIndex HTTP ${res.status}`);
|
|
37
|
+
const premiums = await res.json();
|
|
38
|
+
if (!Array.isArray(premiums))
|
|
39
|
+
throw new Error("Aster premiumIndex returned a non-array response");
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const p of premiums) {
|
|
42
|
+
const rawSym = String(p.symbol ?? "");
|
|
43
|
+
if (!rawSym.endsWith("USDT"))
|
|
44
|
+
continue;
|
|
45
|
+
const fundingRate = Number(p.lastFundingRate);
|
|
46
|
+
const markPrice = Number(p.markPrice);
|
|
47
|
+
if (!Number.isFinite(fundingRate) || !Number.isFinite(markPrice) || markPrice <= 0) {
|
|
48
|
+
continue;
|
|
53
49
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
out.push({
|
|
51
|
+
exchange: "aster",
|
|
52
|
+
symbol: rawSym.replace(/USDT$/, ""),
|
|
53
|
+
fundingRate,
|
|
54
|
+
markPrice,
|
|
55
|
+
});
|
|
58
56
|
}
|
|
57
|
+
return out;
|
|
59
58
|
}
|
|
60
59
|
async function fetchLighterRates() {
|
|
61
60
|
// SSOT rule #2: error must propagate. Caller (handleBasisScan) uses
|
|
@@ -450,7 +449,10 @@ export async function handleBasisScan(isJson, opts) {
|
|
|
450
449
|
export async function handleDexScan(isJson, opts) {
|
|
451
450
|
if (!isJson())
|
|
452
451
|
console.log(chalk.cyan(" Scanning HIP-3 deployed dex funding spreads...\n"));
|
|
453
|
-
const pairs = await scanDexArb({ minAnnualSpread: parseFloat(opts.minSpread), maxPriceGapPct: parseFloat(opts.maxGap), includeNative: opts.includeNative });
|
|
452
|
+
const { pairs, failedDexes } = await scanDexArb({ minAnnualSpread: parseFloat(opts.minSpread), maxPriceGapPct: parseFloat(opts.maxGap), includeNative: opts.includeNative });
|
|
453
|
+
// A dex whose read failed is reported (stderr, both modes — JSON stdout keeps its shape); its pairs are unknown, not absent.
|
|
454
|
+
for (const [dex, msg] of Object.entries(failedDexes))
|
|
455
|
+
process.stderr.write(`[dex-scan] HIP-3 dex ${dex} unavailable: ${msg} — its pairs are missing from this scan\n`);
|
|
454
456
|
const topN = parseInt(opts.top);
|
|
455
457
|
const shown = pairs.slice(0, topN);
|
|
456
458
|
if (isJson())
|
|
@@ -543,10 +545,11 @@ export async function handleFundingPositions(isJson, getAdapterForExchange, opts
|
|
|
543
545
|
const results = [];
|
|
544
546
|
const exchangeErrors = {};
|
|
545
547
|
const fundingUnknown = [];
|
|
548
|
+
/* venue → why its funding history could not be read; its positions get actual* = null (never 0) */ const fundingHistoryErrors = {};
|
|
546
549
|
await Promise.all(targetExchanges.map(async (exchange) => {
|
|
547
550
|
try {
|
|
548
551
|
const adapter = await getAdapterForExchange(exchange);
|
|
549
|
-
const [positions, markets, fp] = await Promise.all([adapter.getPositions(), adapter.getMarkets(), adapter.getFundingPayments(100).
|
|
552
|
+
const [positions, markets, fp] = await Promise.all([adapter.getPositions(), adapter.getMarkets(), adapter.getFundingPayments(100).then((rows) => rows, (e) => { fundingHistoryErrors[exchange] = e instanceof Error ? e.message : String(e); return null; })]);
|
|
550
553
|
if (positions.length === 0)
|
|
551
554
|
return;
|
|
552
555
|
/* rate null = the venue reports no funding rate for the market (never read as 0) */ const fm = new Map();
|
|
@@ -557,11 +560,15 @@ export async function handleFundingPositions(isJson, getAdapterForExchange, opts
|
|
|
557
560
|
}
|
|
558
561
|
const dayAgo = Date.now() - 86400000;
|
|
559
562
|
const af = new Map();
|
|
560
|
-
for (const f of fp) {
|
|
563
|
+
for (const f of fp ?? []) {
|
|
561
564
|
if (f.time < dayAgo)
|
|
562
565
|
continue;
|
|
563
566
|
const sym = f.symbol.toUpperCase().replace(/-PERP$/, "");
|
|
564
|
-
const amt = Number(f.payment)
|
|
567
|
+
const amt = Number(f.payment);
|
|
568
|
+
if (!Number.isFinite(amt)) {
|
|
569
|
+
fundingHistoryErrors[exchange] = `non-numeric funding payment ${JSON.stringify(f.payment)}`;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
565
572
|
if (!af.has(sym))
|
|
566
573
|
af.set(sym, { received: 0, paid: 0 });
|
|
567
574
|
const e = af.get(sym);
|
|
@@ -570,6 +577,7 @@ export async function handleFundingPositions(isJson, getAdapterForExchange, opts
|
|
|
570
577
|
else
|
|
571
578
|
e.paid += Math.abs(amt);
|
|
572
579
|
}
|
|
580
|
+
/* a venue whose history was unreadable or had an unparsable row: actual* = null (never a partial sum shown as complete) */ const histBad = fp === null || fundingHistoryErrors[exchange] !== undefined;
|
|
573
581
|
for (const pos of positions) {
|
|
574
582
|
const sym = pos.symbol.toUpperCase().replace(/-PERP$/, "");
|
|
575
583
|
const fd = fm.get(sym);
|
|
@@ -578,7 +586,12 @@ export async function handleFundingPositions(isJson, getAdapterForExchange, opts
|
|
|
578
586
|
continue;
|
|
579
587
|
}
|
|
580
588
|
const fr = fd.rate;
|
|
581
|
-
const
|
|
589
|
+
const posMark = Number(pos.markPrice);
|
|
590
|
+
const mk = Number.isFinite(posMark) && posMark > 0 ? posMark : fd.markPrice;
|
|
591
|
+
if (!(mk > 0)) {
|
|
592
|
+
fundingUnknown.push(`${exchange}:${sym} (no mark price)`);
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
582
595
|
const sz = Math.abs(Number(pos.size));
|
|
583
596
|
const nu = sz * mk;
|
|
584
597
|
const fh = fd ? fd.fundingHours : null; /* market basis first (§10.1) */
|
|
@@ -586,7 +599,7 @@ export async function handleFundingPositions(isJson, getAdapterForExchange, opts
|
|
|
586
599
|
const ap = annualizeHourlyRate(hr);
|
|
587
600
|
const hp = estimateHourlyFundingFromHourly(hr, nu, pos.side);
|
|
588
601
|
const a = af.get(sym);
|
|
589
|
-
results.push({ exchange, symbol: sym, side: pos.side, size: pos.size, entryPrice: pos.entryPrice, markPrice: pos.markPrice, unrealizedPnl: pos.unrealizedPnl, leverage: pos.leverage, notionalUsd: nu, fundingRate: fr, hourlyRate: hr, annualPct: ap, hourlyPayment: hp, dailyPayment: hp * 24, actualReceived24h: a?.received ?? 0, actualPaid24h: a?.paid ?? 0, actualNet24h: (a?.received ?? 0) - (a?.paid ?? 0) });
|
|
602
|
+
results.push({ exchange, symbol: sym, side: pos.side, size: pos.size, entryPrice: pos.entryPrice, markPrice: pos.markPrice, unrealizedPnl: pos.unrealizedPnl, leverage: pos.leverage, notionalUsd: nu, fundingRate: fr, hourlyRate: hr, annualPct: ap, hourlyPayment: hp, dailyPayment: hp * 24, actualReceived24h: histBad ? null : a?.received ?? 0, actualPaid24h: histBad ? null : a?.paid ?? 0, actualNet24h: histBad ? null : (a?.received ?? 0) - (a?.paid ?? 0) });
|
|
590
603
|
}
|
|
591
604
|
}
|
|
592
605
|
catch (err) {
|
|
@@ -603,15 +616,18 @@ export async function handleFundingPositions(isJson, getAdapterForExchange, opts
|
|
|
603
616
|
const tH = results.reduce((s, r) => s + r.hourlyPayment, 0);
|
|
604
617
|
const tD = results.reduce((s, r) => s + r.dailyPayment, 0);
|
|
605
618
|
const tN = results.reduce((s, r) => s + r.notionalUsd, 0);
|
|
606
|
-
const tA = results.reduce((s, r) => s + r.actualNet24h, 0);
|
|
619
|
+
const tA = results.reduce((s, r) => s + (r.actualNet24h ?? 0), 0);
|
|
620
|
+
const actualIncomplete = results.some(r => r.actualNet24h === null);
|
|
607
621
|
if (isJson())
|
|
608
|
-
return printJson(jsonOk({ positions: results.map(r => ({ exchange: r.exchange, symbol: r.symbol, side: r.side, size: r.size, notionalUsd: +r.notionalUsd.toFixed(2), fundingRate: r.fundingRate, annualPct: +r.annualPct.toFixed(2), predicted: { hourly: +r.hourlyPayment.toFixed(6), daily: +r.dailyPayment.toFixed(4) }, actual24h: { received: +r.actualReceived24h.toFixed(6), paid: +r.actualPaid24h.toFixed(6), net: +r.actualNet24h.toFixed(6) }, unrealizedPnl: r.unrealizedPnl })), totals: { predicted: { hourly: +tH.toFixed(6), daily: +tD.toFixed(4) }, actual24h: { net: +tA.toFixed(6) }, notionalUsd: +tN.toFixed(2) }, fundingUnknown, errors: Object.keys(exchangeErrors).length > 0 ? exchangeErrors : undefined }));
|
|
622
|
+
return printJson(jsonOk({ positions: results.map(r => ({ exchange: r.exchange, symbol: r.symbol, side: r.side, size: r.size, notionalUsd: +r.notionalUsd.toFixed(2), fundingRate: r.fundingRate, annualPct: +r.annualPct.toFixed(2), predicted: { hourly: +r.hourlyPayment.toFixed(6), daily: +r.dailyPayment.toFixed(4) }, actual24h: r.actualNet24h === null ? null : { received: +(r.actualReceived24h ?? 0).toFixed(6), paid: +(r.actualPaid24h ?? 0).toFixed(6), net: +r.actualNet24h.toFixed(6) }, unrealizedPnl: r.unrealizedPnl })), totals: { predicted: { hourly: +tH.toFixed(6), daily: +tD.toFixed(4) }, actual24h: { net: +tA.toFixed(6), incomplete: actualIncomplete }, notionalUsd: +tN.toFixed(2) }, fundingUnknown, fundingHistoryErrors, errors: Object.keys(exchangeErrors).length > 0 ? exchangeErrors : undefined }));
|
|
609
623
|
const exAbbr = venueAbbr;
|
|
610
624
|
console.log(chalk.cyan.bold(" Position Funding Impact\n"));
|
|
611
|
-
const rows = results.map(r => { const sc = r.side === "long" ? chalk.green : chalk.red; const rc = r.fundingRate > 0 ? chalk.red : r.fundingRate < 0 ? chalk.green : chalk.white; const as = r.actualNet24h !== 0 ? (r.actualNet24h > 0 ? chalk.green : chalk.red)(`${r.actualNet24h > 0 ? "+" : ""}$${r.actualNet24h.toFixed(4)}`) : chalk.gray("-"); return [chalk.white.bold(exAbbr(r.exchange)), chalk.white.bold(r.symbol), sc(r.side.toUpperCase()), `$${formatUsd(r.notionalUsd)}`, rc(formatPercent(r.fundingRate)), rc(`${r.annualPct.toFixed(1)}%`), as, formatPnl(r.unrealizedPnl)]; });
|
|
625
|
+
const rows = results.map(r => { const sc = r.side === "long" ? chalk.green : chalk.red; const rc = r.fundingRate > 0 ? chalk.red : r.fundingRate < 0 ? chalk.green : chalk.white; const as = r.actualNet24h === null ? chalk.yellow("unavailable") : r.actualNet24h !== 0 ? (r.actualNet24h > 0 ? chalk.green : chalk.red)(`${r.actualNet24h > 0 ? "+" : ""}$${r.actualNet24h.toFixed(4)}`) : chalk.gray("-"); return [chalk.white.bold(exAbbr(r.exchange)), chalk.white.bold(r.symbol), sc(r.side.toUpperCase()), `$${formatUsd(r.notionalUsd)}`, rc(formatPercent(r.fundingRate)), rc(`${r.annualPct.toFixed(1)}%`), as, formatPnl(r.unrealizedPnl)]; });
|
|
612
626
|
console.log(makeTable(["Ex", "Symbol", "Side", "Notional", "Rate(now)", "Annual(now)", "Actual 24h", "uPnL"], rows));
|
|
613
627
|
console.log(`\n Total: $${formatUsd(tN)} | Actual 24h: ${tA >= 0 ? "+" : ""}$${tA.toFixed(4)} | Predicted: ${tD >= 0 ? "-" : "+"}$${Math.abs(tD).toFixed(4)}/d\n`);
|
|
614
628
|
if (fundingUnknown.length > 0)
|
|
615
629
|
console.log(chalk.yellow(` Funding rate not published (excluded, not counted as 0): ${fundingUnknown.join(", ")}\n`));
|
|
630
|
+
for (const [ex, msg] of Object.entries(fundingHistoryErrors))
|
|
631
|
+
console.log(chalk.yellow(` ${ex} funding history unavailable (Actual 24h excluded, not 0): ${msg}`));
|
|
616
632
|
});
|
|
617
633
|
}
|
package/dist/commands/bot.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import type { ExchangeAdapter } from "../exchanges/index.js";
|
|
3
3
|
export declare function registerStrategyCommands(program: Command, getAdapter: () => Promise<ExchangeAdapter>, getAdapterFor: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean): void;
|
|
4
|
+
/**
|
|
5
|
+
* Adapters for a multi-venue bot. An explicitly passed list (--exchanges on the command line)
|
|
6
|
+
* must load in full: a venue that fails refuses the start with a PerpError naming it. A default
|
|
7
|
+
* list skips a failing venue with a stderr warning naming it and the cause.
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadBotVenueAdapters(names: string[], explicit: boolean, getAdapterFor: (exchange: string) => Promise<ExchangeAdapter>): Promise<Map<string, ExchangeAdapter>>;
|