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
package/dist/commands/account.js
CHANGED
|
@@ -288,7 +288,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
288
288
|
});
|
|
289
289
|
account
|
|
290
290
|
.command("history")
|
|
291
|
-
.description("Order history")
|
|
291
|
+
.description("Order history (Aster: last 7 days, symbols with a position, open order or fill in that window)")
|
|
292
292
|
.action(async () => {
|
|
293
293
|
const adapter = await getAdapter();
|
|
294
294
|
const orders = await adapter.getOrderHistory(30);
|
|
@@ -355,7 +355,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
355
355
|
});
|
|
356
356
|
account
|
|
357
357
|
.command("trades")
|
|
358
|
-
.description("Trade history (fills)")
|
|
358
|
+
.description("Trade history (fills) (Aster: last 7 days, symbols with a position, open order or fill in that window)")
|
|
359
359
|
.action(async () => {
|
|
360
360
|
const adapter = await getAdapter();
|
|
361
361
|
const trades = await adapter.getTradeHistory(30);
|
|
@@ -12,10 +12,30 @@ export declare function listArbVenues(): Promise<string[]>;
|
|
|
12
12
|
* positions read fails is an error (Rule #2): an unread venue is not "no positions" —
|
|
13
13
|
* a hidden leg would make a live pair look closed.
|
|
14
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* Adapters for `arb rebalance`. A venue the user listed explicitly (--exchanges) that cannot
|
|
17
|
+
* be opened is refused with a PerpError naming venue + cause; from the default list it is
|
|
18
|
+
* skipped with a stderr warning naming venue + cause (never silently).
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveRebalanceAdapters(names: readonly string[], explicit: boolean, getAdapter: (exchange: string) => Promise<ExchangeAdapter>, warn?: (msg: string) => void): Promise<Map<string, ExchangeAdapter>>;
|
|
15
21
|
export declare function readArbVenuePositions(venues: readonly string[], getAdapter: (exchange: string) => Promise<ExchangeAdapter>): Promise<{
|
|
16
22
|
exchange: string;
|
|
17
23
|
position: ExchangePosition;
|
|
18
24
|
}[]>;
|
|
25
|
+
/**
|
|
26
|
+
* Spot-perp funding view from the perp venue's current rate entry. A missing entry → every
|
|
27
|
+
* field null ("unknown"): never a 0 rate / 0 daily income / a guessed direction.
|
|
28
|
+
*/
|
|
29
|
+
export declare function spotPerpFundingView(perpRate: {
|
|
30
|
+
exchange: string;
|
|
31
|
+
rate: number;
|
|
32
|
+
fundingHours: number | null;
|
|
33
|
+
} | undefined, perpNotional: number): {
|
|
34
|
+
hourlyRate: number | null;
|
|
35
|
+
annualPct: number | null;
|
|
36
|
+
direction: "long-spot-short-perp" | "sell-spot-long-perp" | null;
|
|
37
|
+
dailyFundingEstimate: number | null;
|
|
38
|
+
};
|
|
19
39
|
/**
|
|
20
40
|
* State-file entries the venues do not confirm: no live pair with the same symbol and
|
|
21
41
|
* legs. `arb status` reports them apart from the live pairs (the venue is the source of truth).
|
|
@@ -7,7 +7,7 @@ import { toHourlyRate } from "../../funding.js";
|
|
|
7
7
|
import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates, } from "../../shared-api.js";
|
|
8
8
|
import { computeBasisRisk } from "../../arb-utils.js";
|
|
9
9
|
import { removePosition as persistRemovePosition, getPositions as getArbStatePositions, loadArbState } from "../../arb-state.js";
|
|
10
|
-
import {
|
|
10
|
+
import { fetchBalancesWithFailures, computeRebalancePlan } from "../../rebalance.js";
|
|
11
11
|
import { EXCHANGE_TO_CHAIN, getBestQuote } from "../../bridge-engine.js";
|
|
12
12
|
import { computeEnhancedStats } from "../../arb-history-stats.js";
|
|
13
13
|
import { listBotSpotVenues, nonQuoteSpotBalances, requireSpotAdapter, spotNetworkOf } from "../../exchanges/spot-factory.js";
|
|
@@ -33,6 +33,30 @@ export async function listArbVenues() {
|
|
|
33
33
|
* positions read fails is an error (Rule #2): an unread venue is not "no positions" —
|
|
34
34
|
* a hidden leg would make a live pair look closed.
|
|
35
35
|
*/
|
|
36
|
+
/**
|
|
37
|
+
* Adapters for `arb rebalance`. A venue the user listed explicitly (--exchanges) that cannot
|
|
38
|
+
* be opened is refused with a PerpError naming venue + cause; from the default list it is
|
|
39
|
+
* skipped with a stderr warning naming venue + cause (never silently).
|
|
40
|
+
*/
|
|
41
|
+
export async function resolveRebalanceAdapters(names, explicit, getAdapter, warn = (m) => process.stderr.write(`${m}\n`)) {
|
|
42
|
+
const out = new Map();
|
|
43
|
+
for (const name of names) {
|
|
44
|
+
try {
|
|
45
|
+
out.set(name, await getAdapter(name));
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
const c = classifyError(e, name);
|
|
49
|
+
if (explicit) {
|
|
50
|
+
throw new PerpError(c.code, `Cannot open ${name}: ${c.message}`, {
|
|
51
|
+
exchange: name,
|
|
52
|
+
remediation: `Check ${name} credentials / connectivity (perp account balance -e ${name}), or drop it from --exchanges`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
warn(`[arb rebalance] ${name} unavailable — skipped: ${c.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
36
60
|
export async function readArbVenuePositions(venues, getAdapter) {
|
|
37
61
|
const out = [];
|
|
38
62
|
for (const exchange of venues) {
|
|
@@ -64,6 +88,21 @@ function formatDuration(ms) {
|
|
|
64
88
|
const minutes = Math.floor(ms / (1000 * 60));
|
|
65
89
|
return `${minutes}m`;
|
|
66
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Spot-perp funding view from the perp venue's current rate entry. A missing entry → every
|
|
93
|
+
* field null ("unknown"): never a 0 rate / 0 daily income / a guessed direction.
|
|
94
|
+
*/
|
|
95
|
+
export function spotPerpFundingView(perpRate, perpNotional) {
|
|
96
|
+
if (perpRate === undefined)
|
|
97
|
+
return { hourlyRate: null, annualPct: null, direction: null, dailyFundingEstimate: null };
|
|
98
|
+
const hourlyRate = entryHourly(perpRate);
|
|
99
|
+
return {
|
|
100
|
+
hourlyRate,
|
|
101
|
+
annualPct: hourlyRate * 8760 * 100,
|
|
102
|
+
direction: hourlyRate >= 0 ? "long-spot-short-perp" : "sell-spot-long-perp",
|
|
103
|
+
dailyFundingEstimate: Math.abs(hourlyRate) * perpNotional * 24,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
67
106
|
/** hourly rate of a rate entry: the entry's own interval (Aster, per symbol) or the venue basis. */
|
|
68
107
|
function entryHourly(e) {
|
|
69
108
|
return e.fundingHours !== null ? e.rate / e.fundingHours : toHourlyRate(e.rate, e.exchange);
|
|
@@ -279,7 +318,9 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
279
318
|
try {
|
|
280
319
|
rateMap = await fetchFundingRatesMap();
|
|
281
320
|
}
|
|
282
|
-
catch {
|
|
321
|
+
catch (e) {
|
|
322
|
+
// Reported, not silent: every spread / funding rate below shows as unknown (null), never 0.
|
|
323
|
+
process.stderr.write(`[warn] funding rates unavailable — spreads and daily funding shown as unknown: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
283
324
|
rateMap = new Map();
|
|
284
325
|
}
|
|
285
326
|
for (const [symbol, positions] of bySymbol) {
|
|
@@ -429,16 +470,18 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
429
470
|
// Get current perp funding rate for this symbol on this exchange
|
|
430
471
|
const rates = rateMap.get(perpSymbol);
|
|
431
472
|
const perpRate = rates?.find(r => r.exchange === shortPos.exchange);
|
|
432
|
-
const fundingRate = perpRate?.rate ?? 0;
|
|
433
|
-
const hourlyRate = perpRate && fundingRate ? entryHourly(perpRate) : 0;
|
|
434
|
-
const annualPct = hourlyRate * 8760 * 100;
|
|
435
473
|
const perpNotional = shortPos.size * shortPos.markPrice;
|
|
474
|
+
// Unknown rate → hourlyRate / annualPct / direction / dailyFundingEstimate are null (never 0).
|
|
475
|
+
const { hourlyRate, annualPct, direction, dailyFundingEstimate } = spotPerpFundingView(perpRate, perpNotional);
|
|
436
476
|
const spotValueUsd = arbSpotAmount * shortPos.markPrice; // approx using perp mark price
|
|
437
477
|
// Funding income: fetch actual payments from exchange API
|
|
438
478
|
let actualFundingIncome = 0;
|
|
479
|
+
let fundingSource = "actual";
|
|
480
|
+
let fundingError = null;
|
|
439
481
|
try {
|
|
440
482
|
const perpAdapter = await getAdapterForExchange(shortPos.exchange);
|
|
441
|
-
const
|
|
483
|
+
const FUNDING_PAGE = 200;
|
|
484
|
+
const payments = await perpAdapter.getFundingPayments(FUNDING_PAGE);
|
|
442
485
|
// Filter from entry time minus 1h buffer (multi-leg entry takes time)
|
|
443
486
|
const entryTime = persisted ? new Date(persisted.entryTime).getTime() - 3600000 : 0;
|
|
444
487
|
for (const p of payments) {
|
|
@@ -446,10 +489,22 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
446
489
|
actualFundingIncome += Number(p.payment);
|
|
447
490
|
}
|
|
448
491
|
}
|
|
492
|
+
// The read may not reach back to the entry: a full page, or Aster's 7-day income window.
|
|
493
|
+
const sevenDaysAgo = Date.now() - 7 * 86_400_000;
|
|
494
|
+
if (payments.length >= FUNDING_PAGE) {
|
|
495
|
+
fundingSource = "partial";
|
|
496
|
+
fundingError = `${FUNDING_PAGE} payments returned (page full) — older funding since entry may be missing`;
|
|
497
|
+
}
|
|
498
|
+
else if (shortPos.exchange === "aster" && (!entryTime || entryTime < sevenDaysAgo)) {
|
|
499
|
+
fundingSource = "partial";
|
|
500
|
+
fundingError = "Aster funding history covers the last 7 days only — funding before that is not included";
|
|
501
|
+
}
|
|
449
502
|
}
|
|
450
|
-
catch {
|
|
451
|
-
//
|
|
452
|
-
|
|
503
|
+
catch (e) {
|
|
504
|
+
// History unreadable: an explicit, labelled estimate (fundingSource / fundingError), never a silent one.
|
|
505
|
+
fundingSource = "estimate";
|
|
506
|
+
fundingError = e instanceof Error ? e.message : String(e);
|
|
507
|
+
if (holdDurationMs && hourlyRate !== null && hourlyRate !== 0) {
|
|
453
508
|
const holdHours = holdDurationMs / (1000 * 60 * 60);
|
|
454
509
|
actualFundingIncome = Math.abs(hourlyRate) * perpNotional * holdHours;
|
|
455
510
|
if (hourlyRate < 0)
|
|
@@ -462,8 +517,6 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
462
517
|
const spotFee = getTakerFee(spotEx);
|
|
463
518
|
const perpFee = getTakerFee(shortPos.exchange);
|
|
464
519
|
const entryFees = (spotValueUsd * spotFee) + (perpNotional * perpFee);
|
|
465
|
-
const direction = hourlyRate >= 0 ? "long-spot-short-perp" : "sell-spot-long-perp";
|
|
466
|
-
const dailyFundingEstimate = Math.abs(hourlyRate) * perpNotional * 24;
|
|
467
520
|
spotPerpPositions.push({
|
|
468
521
|
symbol: perpSymbol,
|
|
469
522
|
mode: "spot-perp",
|
|
@@ -483,10 +536,12 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
483
536
|
perpLeverage: shortPos.leverage,
|
|
484
537
|
perpNotionalUsd: perpNotional,
|
|
485
538
|
direction,
|
|
486
|
-
currentSpread: annualPct
|
|
539
|
+
currentSpread: annualPct,
|
|
487
540
|
holdDuration,
|
|
488
541
|
holdDurationMs,
|
|
489
542
|
estimatedFundingIncome,
|
|
543
|
+
fundingSource,
|
|
544
|
+
fundingError,
|
|
490
545
|
estimatedFees: entryFees,
|
|
491
546
|
netPnl: (spotValueUsd - shortPos.entryPrice * arbSpotAmount) + shortPos.unrealizedPnl + estimatedFundingIncome - entryFees,
|
|
492
547
|
dailyFundingEstimate,
|
|
@@ -507,8 +562,8 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
507
562
|
const holdDurationMs = Date.now() - new Date(persisted.entryTime).getTime();
|
|
508
563
|
const rates = rateMap.get(sym);
|
|
509
564
|
const perpRate = rates?.find(r => r.exchange === persisted.shortExchange);
|
|
510
|
-
const hourlyRate = perpRate ? entryHourly(perpRate) : 0;
|
|
511
565
|
const perpNotional = shortPos.size * shortPos.markPrice;
|
|
566
|
+
const fv = spotPerpFundingView(perpRate, perpNotional);
|
|
512
567
|
spotPerpPositions.push({
|
|
513
568
|
symbol: sym,
|
|
514
569
|
mode: "spot-perp",
|
|
@@ -527,14 +582,16 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
527
582
|
perpUnrealizedPnl: shortPos.unrealizedPnl,
|
|
528
583
|
perpLeverage: shortPos.leverage,
|
|
529
584
|
perpNotionalUsd: perpNotional,
|
|
530
|
-
direction:
|
|
531
|
-
currentSpread:
|
|
585
|
+
direction: fv.direction,
|
|
586
|
+
currentSpread: fv.annualPct === null ? null : Math.abs(fv.annualPct),
|
|
532
587
|
holdDuration: formatDuration(holdDurationMs),
|
|
533
588
|
holdDurationMs,
|
|
534
589
|
estimatedFundingIncome: persisted.accumulatedFunding ?? 0,
|
|
590
|
+
fundingSource: "recorded",
|
|
591
|
+
fundingError: null,
|
|
535
592
|
estimatedFees: 0,
|
|
536
593
|
netPnl: shortPos.unrealizedPnl + (persisted.accumulatedFunding ?? 0),
|
|
537
|
-
dailyFundingEstimate:
|
|
594
|
+
dailyFundingEstimate: fv.dailyFundingEstimate,
|
|
538
595
|
});
|
|
539
596
|
}
|
|
540
597
|
// State vs venue: the venue is the source of truth. A pair is reported only when the
|
|
@@ -544,12 +601,13 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
544
601
|
// Pairs only inferred from a long and a short sharing the symbol are reported apart (never as arb positions).
|
|
545
602
|
const unconfirmedPairs = arbPairs.filter(p => p.provenance === "inferred");
|
|
546
603
|
arbPairs.splice(0, arbPairs.length, ...arbPairs.filter(p => p.provenance !== "inferred"));
|
|
604
|
+
const fundingUnknownSP = spotPerpPositions.filter(p => p.dailyFundingEstimate === null).map(p => `${p.perpExchange}:${p.symbol}`);
|
|
547
605
|
// ── JSON output ──
|
|
548
606
|
if (isJson()) {
|
|
549
607
|
const totalNotionalPP = arbPairs.reduce((s, p) => s + (p.longPosition.notionalUsd + p.shortPosition.notionalUsd) / 2, 0);
|
|
550
608
|
const totalDailyPP = arbPairs.reduce((s, p) => s + p.dailyFundingEstimate, 0);
|
|
551
609
|
const totalNotionalSP = spotPerpPositions.reduce((s, p) => s + p.perpNotionalUsd, 0);
|
|
552
|
-
const totalDailySP = spotPerpPositions.reduce((s, p) => s + p.dailyFundingEstimate, 0);
|
|
610
|
+
const totalDailySP = spotPerpPositions.reduce((s, p) => s + (p.dailyFundingEstimate ?? 0), 0);
|
|
553
611
|
const totalNotionalJ = totalNotionalPP + totalNotionalSP;
|
|
554
612
|
const totalDailyJ = totalDailyPP + totalDailySP;
|
|
555
613
|
return printJson(jsonOk({
|
|
@@ -569,6 +627,9 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
569
627
|
netPnl: arbPairs.reduce((s, p) => s + p.netPnl, 0) + spotPerpPositions.reduce((s, p) => s + p.netPnl, 0),
|
|
570
628
|
dailyFundingEstimate: totalDailyJ,
|
|
571
629
|
annualizedApr: totalNotionalJ > 0 ? (totalDailyJ * 365 / totalNotionalJ) * 100 : 0,
|
|
630
|
+
// Spot-perp positions whose funding rate is unknown are excluded from the daily / APR sums.
|
|
631
|
+
dailyFundingIncomplete: fundingUnknownSP.length > 0,
|
|
632
|
+
fundingUnknown: fundingUnknownSP,
|
|
572
633
|
},
|
|
573
634
|
}));
|
|
574
635
|
}
|
|
@@ -647,12 +708,12 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
647
708
|
console.log(chalk.white.bold(" PnL Breakdown:"));
|
|
648
709
|
console.log(` Spot PnL: ${formatPnl(spotPnl)}`);
|
|
649
710
|
console.log(` Perp PnL: ${formatPnl(sp.perpUnrealizedPnl)}`);
|
|
650
|
-
console.log(` Funding Earned: ${chalk.yellow(`$${sp.estimatedFundingIncome.toFixed(4)}`)}`);
|
|
711
|
+
console.log(` Funding Earned: ${chalk.yellow(`$${sp.estimatedFundingIncome.toFixed(4)}`)}${sp.fundingSource === "estimate" ? chalk.yellow(` (estimate — funding history unavailable: ${sp.fundingError})`) : sp.fundingSource === "partial" ? chalk.yellow(` (partial: ${sp.fundingError})`) : ""}`);
|
|
651
712
|
console.log(` Trading Fees: ${chalk.red(`-$${sp.estimatedFees.toFixed(4)}`)}`);
|
|
652
713
|
console.log(` ${chalk.white.bold("Total PnL:")} ${formatPnl(sp.netPnl)}${roi !== 0 ? chalk.gray(` (ROI: ${roi >= 0 ? "+" : ""}${roi.toFixed(2)}%)`) : ""}`);
|
|
653
714
|
console.log("");
|
|
654
715
|
console.log(` Current Spread: ${spreadStr}`);
|
|
655
|
-
console.log(` Daily Income: ${chalk.cyan(`$${sp.dailyFundingEstimate.toFixed(4)}/day`)}`);
|
|
716
|
+
console.log(` Daily Income: ${sp.dailyFundingEstimate === null ? chalk.yellow(`unknown (no current ${sp.perpExchange} funding rate)`) : chalk.cyan(`$${sp.dailyFundingEstimate.toFixed(4)}/day`)}`);
|
|
656
717
|
if (annRoi !== 0)
|
|
657
718
|
console.log(` Annualized ROI: ${chalk.cyan(`${annRoi >= 0 ? "+" : ""}${annRoi.toFixed(1)}%`)}`);
|
|
658
719
|
}
|
|
@@ -668,7 +729,7 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
668
729
|
const totalNet = arbPairs.reduce((s, p) => s + p.netPnl, 0)
|
|
669
730
|
+ spotPerpPositions.reduce((s, p) => s + p.netPnl, 0);
|
|
670
731
|
const totalDailyFunding = arbPairs.reduce((s, p) => s + p.dailyFundingEstimate, 0)
|
|
671
|
-
+ spotPerpPositions.reduce((s, p) => s + p.dailyFundingEstimate, 0);
|
|
732
|
+
+ spotPerpPositions.reduce((s, p) => s + (p.dailyFundingEstimate ?? 0), 0);
|
|
672
733
|
const totalNotional = arbPairs.reduce((s, p) => s + (p.longPosition.notionalUsd + p.shortPosition.notionalUsd) / 2, 0)
|
|
673
734
|
+ spotPerpPositions.reduce((s, p) => s + p.perpNotionalUsd, 0);
|
|
674
735
|
const portfolioApr = totalNotional > 0 ? (totalDailyFunding * 365 / totalNotional) * 100 : 0;
|
|
@@ -680,7 +741,7 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
680
741
|
console.log(` Est. Funding: ${chalk.yellow(`$${totalFunding.toFixed(4)}`)}`);
|
|
681
742
|
console.log(` Est. Fees: ${chalk.red(`-$${totalFees.toFixed(4)}`)}`);
|
|
682
743
|
console.log(` Net PnL: ${formatPnl(totalNet)}`);
|
|
683
|
-
console.log(` Daily Income: ${chalk.cyan(`$${totalDailyFunding.toFixed(4)}/day`)}`);
|
|
744
|
+
console.log(` Daily Income: ${chalk.cyan(`$${totalDailyFunding.toFixed(4)}/day`)}${fundingUnknownSP.length > 0 ? chalk.yellow(` (incomplete — funding rate unknown: ${fundingUnknownSP.join(", ")})`) : ""}`);
|
|
684
745
|
if (portfolioApr > 0) {
|
|
685
746
|
console.log(` Portfolio APR: ${chalk.cyan(`${portfolioApr.toFixed(1)}%`)}`);
|
|
686
747
|
}
|
|
@@ -1296,12 +1357,10 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
1296
1357
|
const dryRun = isDryRun(cmd);
|
|
1297
1358
|
const exchangeNames = opts.exchanges.split(",").map(e => e.trim());
|
|
1298
1359
|
const adapters = new Map();
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
catch { /* skip unavailable */ }
|
|
1304
|
-
}
|
|
1360
|
+
// Explicit --exchanges → an unavailable venue refuses (PerpError); the default list → skipped with a stderr warning.
|
|
1361
|
+
const explicitExchanges = cmd.getOptionValueSource("exchanges") === "cli";
|
|
1362
|
+
for (const [name, adapter] of await resolveRebalanceAdapters(exchangeNames, explicitExchanges, getAdapterForExchange))
|
|
1363
|
+
adapters.set(name, adapter);
|
|
1305
1364
|
if (adapters.size === 0) {
|
|
1306
1365
|
if (isJson())
|
|
1307
1366
|
return printJson(jsonOk({ error: "No exchanges available" }));
|
|
@@ -1312,7 +1371,8 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
1312
1371
|
if (!opts.target && !opts.check) {
|
|
1313
1372
|
opts.check = true;
|
|
1314
1373
|
}
|
|
1315
|
-
|
|
1374
|
+
// A venue whose balance read failed is reported (never dropped from the totals silently).
|
|
1375
|
+
const { snapshots, failed: unavailable, notConfigured } = await fetchBalancesWithFailures(adapters);
|
|
1316
1376
|
const totalEquity = snapshots.reduce((s, e) => s + e.equity, 0);
|
|
1317
1377
|
const totalAvailable = snapshots.reduce((s, e) => s + e.available, 0);
|
|
1318
1378
|
const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : e === "aster" ? "AST" : e.toUpperCase();
|
|
@@ -1333,6 +1393,9 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
1333
1393
|
})),
|
|
1334
1394
|
totalEquity,
|
|
1335
1395
|
totalAvailable,
|
|
1396
|
+
unavailable,
|
|
1397
|
+
notConfigured,
|
|
1398
|
+
partial: unavailable.length > 0,
|
|
1336
1399
|
}));
|
|
1337
1400
|
}
|
|
1338
1401
|
console.log(chalk.cyan("\n Cross-Exchange Balance Distribution\n"));
|
|
@@ -1355,8 +1418,20 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
1355
1418
|
console.log(` Total Equity: $${formatUsd(totalEquity)}`);
|
|
1356
1419
|
console.log(` Total Available: $${formatUsd(totalAvailable)}`);
|
|
1357
1420
|
console.log(` Exchanges: ${snapshots.length}\n`);
|
|
1421
|
+
for (const f of unavailable)
|
|
1422
|
+
console.log(chalk.yellow(` ⚠ ${f.exchange} balance unavailable (not in the totals): ${f.error}`));
|
|
1423
|
+
for (const n of notConfigured)
|
|
1424
|
+
console.log(chalk.gray(` ${n}: not configured (no credentials) — left out`));
|
|
1425
|
+
if (unavailable.length > 0)
|
|
1426
|
+
console.log();
|
|
1358
1427
|
return;
|
|
1359
1428
|
}
|
|
1429
|
+
// A plan over fewer venues than requested would move funds on a wrong picture — refuse.
|
|
1430
|
+
if (unavailable.length > 0) {
|
|
1431
|
+
throw new PerpError("EXCHANGE_ERROR", `Rebalance plan refused: balance unavailable on ${unavailable.map(f => f.exchange).join(", ")}`, {
|
|
1432
|
+
remediation: `Retry when every venue answers, or limit the plan to readable venues with --exchanges (errors: ${unavailable.map(f => `${f.exchange}: ${f.error}`).join("; ")})`,
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1360
1435
|
// Parse target ratios
|
|
1361
1436
|
if (!opts.target) {
|
|
1362
1437
|
console.error(chalk.red(" --target required (e.g., '50:50' or '33:33:33')"));
|
|
@@ -1394,9 +1469,11 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
1394
1469
|
const moveDetails = await Promise.all(moves.map(async (m) => {
|
|
1395
1470
|
const srcChain = EXCHANGE_TO_CHAIN[m.from] ?? "unknown";
|
|
1396
1471
|
const dstChain = EXCHANGE_TO_CHAIN[m.to] ?? "unknown";
|
|
1472
|
+
// null = no quote could be read (quoteError says why) — never a made-up fee / provider.
|
|
1397
1473
|
let bridgeFee = 0;
|
|
1398
1474
|
let bridgeProvider = "same-chain";
|
|
1399
1475
|
let bridgeTime = "instant";
|
|
1476
|
+
let quoteError = null;
|
|
1400
1477
|
if (srcChain !== dstChain && srcChain !== "unknown" && dstChain !== "unknown") {
|
|
1401
1478
|
try {
|
|
1402
1479
|
const quote = await getBestQuote(srcChain, dstChain, m.amount, "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000");
|
|
@@ -1404,10 +1481,11 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
1404
1481
|
bridgeProvider = quote.provider;
|
|
1405
1482
|
bridgeTime = `~${Math.ceil(quote.estimatedTime / 60)}min`;
|
|
1406
1483
|
}
|
|
1407
|
-
catch {
|
|
1408
|
-
bridgeFee =
|
|
1409
|
-
bridgeProvider =
|
|
1410
|
-
bridgeTime =
|
|
1484
|
+
catch (e) {
|
|
1485
|
+
bridgeFee = null;
|
|
1486
|
+
bridgeProvider = null;
|
|
1487
|
+
bridgeTime = null;
|
|
1488
|
+
quoteError = e instanceof Error ? e.message : String(e);
|
|
1411
1489
|
}
|
|
1412
1490
|
}
|
|
1413
1491
|
return {
|
|
@@ -1417,6 +1495,7 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
1417
1495
|
bridgeFee,
|
|
1418
1496
|
bridgeProvider,
|
|
1419
1497
|
bridgeTime,
|
|
1498
|
+
quoteError,
|
|
1420
1499
|
};
|
|
1421
1500
|
}));
|
|
1422
1501
|
if (isJson()) {
|
|
@@ -1453,12 +1532,18 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
1453
1532
|
for (let i = 0; i < moveDetails.length; i++) {
|
|
1454
1533
|
const m = moveDetails[i];
|
|
1455
1534
|
console.log(chalk.white.bold(` Move ${i + 1}: $${m.amount} ${exAbbr(m.from)} -> ${exAbbr(m.to)}`));
|
|
1456
|
-
|
|
1457
|
-
|
|
1535
|
+
if (m.bridgeFee === null) {
|
|
1536
|
+
console.log(chalk.yellow(` Route: ${m.srcChain} -> ${m.dstChain} — no bridge quote (${m.quoteError})`));
|
|
1537
|
+
}
|
|
1538
|
+
else {
|
|
1539
|
+
console.log(chalk.gray(` Route: ${m.srcChain} -> ${m.dstChain} via ${m.bridgeProvider}`));
|
|
1540
|
+
console.log(chalk.gray(` Fee: ~$${m.bridgeFee.toFixed(2)} | Time: ${m.bridgeTime}`));
|
|
1541
|
+
}
|
|
1458
1542
|
console.log();
|
|
1459
1543
|
}
|
|
1460
|
-
const totalFees = moveDetails.reduce((s, m) => s + m.bridgeFee, 0);
|
|
1461
|
-
|
|
1544
|
+
const totalFees = moveDetails.reduce((s, m) => s + (m.bridgeFee ?? 0), 0);
|
|
1545
|
+
const unquoted = moveDetails.filter(m => m.bridgeFee === null).length;
|
|
1546
|
+
console.log(chalk.gray(` Total bridge fees: ~$${totalFees.toFixed(2)}`) + (unquoted > 0 ? chalk.yellow(` (excludes ${unquoted} move(s) without a quote)`) : ""));
|
|
1462
1547
|
if (dryRun) {
|
|
1463
1548
|
console.log(chalk.yellow("\n [DRY RUN] No transfers executed.\n"));
|
|
1464
1549
|
}
|
|
@@ -180,6 +180,42 @@ export declare function isNearSettlement(longExchange: string, shortExchange: st
|
|
|
180
180
|
* meaning we're now paying on both sides instead of collecting.
|
|
181
181
|
*/
|
|
182
182
|
export declare function isSpreadReversed(longExchange: string, shortExchange: string, snapshot: FundingSnapshot): boolean;
|
|
183
|
+
export interface ArbPnlPosition {
|
|
184
|
+
exchange: string;
|
|
185
|
+
symbol: string;
|
|
186
|
+
side: string;
|
|
187
|
+
size: number;
|
|
188
|
+
entry: number;
|
|
189
|
+
mark: number;
|
|
190
|
+
upnl: number;
|
|
191
|
+
lev: number;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Read open positions on each venue for `arb pnl`. A venue whose adapter/positions read
|
|
195
|
+
* fails lands in `positionErrors` (never silently dropped); a venue with no credentials
|
|
196
|
+
* (read-only adapter) lands in `notConfigured`.
|
|
197
|
+
*/
|
|
198
|
+
export declare function collectArbPnlPositions(exchangeNames: string[], getAdapterForExchange: (exchange: string) => Promise<ExchangeAdapter>): Promise<{
|
|
199
|
+
positions: ArbPnlPosition[];
|
|
200
|
+
positionErrors: Record<string, string>;
|
|
201
|
+
notConfigured: string[];
|
|
202
|
+
}>;
|
|
203
|
+
/**
|
|
204
|
+
* "arb" when the symbol has both a long and a short leg; otherwise "unknown" if any venue's
|
|
205
|
+
* positions were unread (its missing leg may be there — never call it naked), else "single".
|
|
206
|
+
*/
|
|
207
|
+
export declare function arbPnlPairStatus(positions: Array<{
|
|
208
|
+
side: string;
|
|
209
|
+
}>, failedVenues: string[]): "arb" | "single" | "unknown";
|
|
210
|
+
/**
|
|
211
|
+
* HIP-3 auto-arb: the dex (either leg) whose market read failed this cycle, or null.
|
|
212
|
+
* A non-null result means the pair's spread is UNKNOWN — the position is kept (never
|
|
213
|
+
* closed as "pair disappeared"); only a pair missing from a successfully read dex is gone.
|
|
214
|
+
*/
|
|
215
|
+
export declare function dexCloseBlockedBy(pos: {
|
|
216
|
+
longDex: string;
|
|
217
|
+
shortDex: string;
|
|
218
|
+
}, failedDexes: Record<string, string>): string | null;
|
|
183
219
|
export declare let handleSpotClose: ((sym: string, opts: {
|
|
184
220
|
spotExch?: string;
|
|
185
221
|
perpExch?: string;
|