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/history.js
CHANGED
|
@@ -359,26 +359,39 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
|
|
|
359
359
|
const limit = parseInt(opts.limit);
|
|
360
360
|
const allTrades = [];
|
|
361
361
|
const allFunding = [];
|
|
362
|
+
// A venue whose adapter / trades / funding read failed is listed here and left out of the
|
|
363
|
+
// totals (partial: true) — never counted as 0.
|
|
364
|
+
const errors = {};
|
|
365
|
+
const errText = (e) => (e instanceof Error ? e.message : String(e));
|
|
362
366
|
// Fetch trades + funding in parallel from all exchanges
|
|
363
367
|
await Promise.allSettled(exchanges.map(async (ex) => {
|
|
368
|
+
let adapter;
|
|
364
369
|
try {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
370
|
+
adapter = await getAdapterForExchange(ex);
|
|
371
|
+
}
|
|
372
|
+
catch (e) {
|
|
373
|
+
errors[`${ex}.adapter`] = errText(e);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const [trades, funding] = await Promise.allSettled([adapter.getTradeHistory(limit), adapter.getFundingPayments(limit)]);
|
|
377
|
+
if (trades.status === "fulfilled") {
|
|
378
|
+
for (const t of trades.value) {
|
|
371
379
|
if (sinceMs && t.time < sinceMs)
|
|
372
380
|
continue;
|
|
373
381
|
allTrades.push({ exchange: ex, symbol: t.symbol, side: t.side, price: Number(t.price), size: Number(t.size), fee: Number(t.fee), time: t.time });
|
|
374
382
|
}
|
|
375
|
-
|
|
383
|
+
}
|
|
384
|
+
else
|
|
385
|
+
errors[`${ex}.trades`] = errText(trades.reason);
|
|
386
|
+
if (funding.status === "fulfilled") {
|
|
387
|
+
for (const f of funding.value) {
|
|
376
388
|
if (sinceMs && f.time < sinceMs)
|
|
377
389
|
continue;
|
|
378
390
|
allFunding.push({ exchange: ex, symbol: f.symbol, payment: Number(f.payment), time: f.time });
|
|
379
391
|
}
|
|
380
392
|
}
|
|
381
|
-
|
|
393
|
+
else
|
|
394
|
+
errors[`${ex}.funding`] = errText(funding.reason);
|
|
382
395
|
}));
|
|
383
396
|
// Aggregate by exchange
|
|
384
397
|
const byExchange = new Map();
|
|
@@ -428,6 +441,8 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
|
|
|
428
441
|
netPnl,
|
|
429
442
|
byExchange: Object.fromEntries([...byExchange.entries()].map(([ex, d]) => [ex, { ...d, netPnl: d.funding - d.fees }])),
|
|
430
443
|
daily,
|
|
444
|
+
errors,
|
|
445
|
+
partial: Object.keys(errors).length > 0,
|
|
431
446
|
};
|
|
432
447
|
if (isJson())
|
|
433
448
|
return printJson(jsonOk(result));
|
|
@@ -438,6 +453,8 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
|
|
|
438
453
|
console.log(` Trading Fees: ${chalk.red(`-$${formatUsd(totalFees)}`)}`);
|
|
439
454
|
console.log(` Funding Income: ${formatPnl(totalFunding)}`);
|
|
440
455
|
console.log(` Net P&L: ${formatPnl(netPnl)}`);
|
|
456
|
+
for (const [k, msg] of Object.entries(errors))
|
|
457
|
+
console.log(chalk.yellow(` ⚠ ${k} unavailable (left out of the totals): ${msg}`));
|
|
441
458
|
if (byExchange.size > 0) {
|
|
442
459
|
console.log(chalk.white.bold("\n By Exchange:"));
|
|
443
460
|
const rows = [...byExchange.entries()].map(([ex, d]) => [
|
|
@@ -480,27 +497,44 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
|
|
|
480
497
|
const allFunding = [];
|
|
481
498
|
// Fetch funding payments and current positions in parallel
|
|
482
499
|
const positionNotionals = new Map();
|
|
500
|
+
// A venue whose adapter / funding / positions read failed is listed here; its funding is
|
|
501
|
+
// left out of the totals (partial: true) — never counted as 0.
|
|
502
|
+
const errors = {};
|
|
503
|
+
const errText = (e) => (e instanceof Error ? e.message : String(e));
|
|
483
504
|
await Promise.allSettled(exchanges.map(async (ex) => {
|
|
505
|
+
let adapter;
|
|
484
506
|
try {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
507
|
+
adapter = await getAdapterForExchange(ex);
|
|
508
|
+
}
|
|
509
|
+
catch (e) {
|
|
510
|
+
errors[`${ex}.adapter`] = errText(e);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const [payments, positions] = await Promise.allSettled([
|
|
514
|
+
adapter.getFundingPayments(parseInt(opts.limit)),
|
|
515
|
+
adapter.getPositions(),
|
|
516
|
+
]);
|
|
517
|
+
if (payments.status === "fulfilled") {
|
|
518
|
+
for (const p of payments.value) {
|
|
491
519
|
if (sinceMs && p.time < sinceMs)
|
|
492
520
|
continue;
|
|
493
521
|
if (opts.symbol && !p.symbol.toUpperCase().includes(opts.symbol.toUpperCase()))
|
|
494
522
|
continue;
|
|
495
523
|
allFunding.push({ exchange: ex, symbol: p.symbol, payment: Number(p.payment), time: p.time });
|
|
496
524
|
}
|
|
497
|
-
|
|
525
|
+
}
|
|
526
|
+
else
|
|
527
|
+
errors[`${ex}.funding`] = errText(payments.reason);
|
|
528
|
+
// Positions only feed the annualized rate (null when the notional is unknown).
|
|
529
|
+
if (positions.status === "fulfilled") {
|
|
530
|
+
for (const p of positions.value) {
|
|
498
531
|
if (Number(p.size) > 0) {
|
|
499
532
|
positionNotionals.set(`${ex}:${p.symbol}`, Math.abs(Number(p.size) * Number(p.markPrice)));
|
|
500
533
|
}
|
|
501
534
|
}
|
|
502
535
|
}
|
|
503
|
-
|
|
536
|
+
else
|
|
537
|
+
errors[`${ex}.positions`] = errText(positions.reason);
|
|
504
538
|
}));
|
|
505
539
|
allFunding.sort((a, b) => a.time - b.time);
|
|
506
540
|
// Aggregate by exchange×symbol
|
|
@@ -556,16 +590,19 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
|
|
|
556
590
|
const arbRows = arbPositions
|
|
557
591
|
.map(pos => {
|
|
558
592
|
const af = arbFunding.get(pos.symbol);
|
|
559
|
-
|
|
593
|
+
// A leg on a venue whose funding read failed is unknown (null), not 0.
|
|
594
|
+
const longFunding = af && !(`${pos.longExchange}.adapter` in errors || `${pos.longExchange}.funding` in errors) ? af.longFunding : null;
|
|
595
|
+
const shortFunding = af && !(`${pos.shortExchange}.adapter` in errors || `${pos.shortExchange}.funding` in errors) ? af.shortFunding : null;
|
|
596
|
+
if (!af || (longFunding === 0 && shortFunding === 0))
|
|
560
597
|
return null;
|
|
561
598
|
return {
|
|
562
599
|
symbol: pos.symbol,
|
|
563
600
|
mode: pos.mode ?? "perp-perp",
|
|
564
601
|
longExchange: pos.longExchange,
|
|
565
602
|
shortExchange: pos.shortExchange,
|
|
566
|
-
netFunding:
|
|
567
|
-
longFunding
|
|
568
|
-
shortFunding
|
|
603
|
+
netFunding: longFunding !== null && shortFunding !== null ? longFunding + shortFunding : null,
|
|
604
|
+
longFunding,
|
|
605
|
+
shortFunding,
|
|
569
606
|
daysSinceEntry: Math.max(1, Math.round((Date.now() - new Date(pos.entryTime).getTime()) / 86400000)),
|
|
570
607
|
};
|
|
571
608
|
})
|
|
@@ -580,6 +617,8 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
|
|
|
580
617
|
byExchange: Object.fromEntries(byExchange),
|
|
581
618
|
arbPositions: arbRows,
|
|
582
619
|
...(opts.daily ? { daily } : {}),
|
|
620
|
+
errors,
|
|
621
|
+
partial: Object.keys(errors).length > 0,
|
|
583
622
|
};
|
|
584
623
|
if (isJson())
|
|
585
624
|
return printJson(jsonOk(result));
|
|
@@ -588,6 +627,8 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
|
|
|
588
627
|
console.log(` Total Payments: ${allFunding.length}`);
|
|
589
628
|
console.log(` Net Funding: ${formatPnl(totalFunding)}`);
|
|
590
629
|
console.log(` Period: ~${Math.round(periodDays)} days`);
|
|
630
|
+
for (const [k, msg] of Object.entries(errors))
|
|
631
|
+
console.log(chalk.yellow(` ⚠ ${k} unavailable (left out of the totals): ${msg}`));
|
|
591
632
|
if (exSymRows.length > 0) {
|
|
592
633
|
console.log(chalk.white.bold("\n By Exchange × Symbol:"));
|
|
593
634
|
const rows = exSymRows.map(e => [
|
|
@@ -605,7 +646,7 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
|
|
|
605
646
|
chalk.white.bold(a.symbol),
|
|
606
647
|
`${a.longExchange} → ${a.shortExchange}`,
|
|
607
648
|
a.mode,
|
|
608
|
-
formatPnl(a.netFunding),
|
|
649
|
+
a.netFunding !== null ? formatPnl(a.netFunding) : chalk.yellow("unavailable"),
|
|
609
650
|
`${a.daysSinceEntry}d`,
|
|
610
651
|
]);
|
|
611
652
|
console.log(makeTable(["Symbol", "Long → Short", "Mode", "Net Funding", "Held"], rows));
|
|
@@ -647,24 +688,44 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
|
|
|
647
688
|
let totalEquity = 0;
|
|
648
689
|
let totalUPnl = 0;
|
|
649
690
|
const symbolSet = new Set();
|
|
691
|
+
// A venue whose read failed is reported here and left out of the totals — never counted as 0.
|
|
692
|
+
const errors = {};
|
|
693
|
+
const errText = (e) => (e instanceof Error ? e.message : String(e));
|
|
694
|
+
let venuesRead = 0;
|
|
650
695
|
await Promise.allSettled(exchanges.map(async (ex) => {
|
|
696
|
+
let adapter;
|
|
697
|
+
try {
|
|
698
|
+
adapter = await getAdapterForExchange(ex);
|
|
699
|
+
}
|
|
700
|
+
catch (e) {
|
|
701
|
+
errors[`${ex}.adapter`] = errText(e);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
651
704
|
try {
|
|
652
|
-
const
|
|
653
|
-
const [balance, trades, funding] = await Promise.all([
|
|
654
|
-
adapter.getBalance(),
|
|
655
|
-
adapter.getTradeHistory(50).catch(() => []),
|
|
656
|
-
adapter.getFundingPayments(50).catch(() => []),
|
|
657
|
-
]);
|
|
705
|
+
const balance = await adapter.getBalance();
|
|
658
706
|
totalEquity += Number(balance.equity);
|
|
659
707
|
totalUPnl += Number(balance.unrealizedPnl);
|
|
660
|
-
|
|
708
|
+
venuesRead++;
|
|
709
|
+
}
|
|
710
|
+
catch (e) {
|
|
711
|
+
errors[`${ex}.balance`] = errText(e);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const [trades, funding] = await Promise.allSettled([adapter.getTradeHistory(50), adapter.getFundingPayments(50)]);
|
|
715
|
+
if (trades.status === "fulfilled") {
|
|
716
|
+
for (const t of trades.value) {
|
|
661
717
|
totalFees += Number(t.fee);
|
|
662
718
|
symbolSet.add(t.symbol);
|
|
663
719
|
}
|
|
664
|
-
|
|
720
|
+
}
|
|
721
|
+
else
|
|
722
|
+
errors[`${ex}.trades`] = errText(trades.reason);
|
|
723
|
+
if (funding.status === "fulfilled") {
|
|
724
|
+
for (const f of funding.value)
|
|
665
725
|
totalFunding += Number(f.payment);
|
|
666
726
|
}
|
|
667
|
-
|
|
727
|
+
else
|
|
728
|
+
errors[`${ex}.funding`] = errText(funding.reason);
|
|
668
729
|
}));
|
|
669
730
|
const report = {
|
|
670
731
|
period: opts.since ?? "all time",
|
|
@@ -691,6 +752,11 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
|
|
|
691
752
|
? (stats.totalTrades / Math.max(1, (Date.now() - new Date(records[records.length - 1]?.timestamp ?? Date.now()).getTime()) / 86400000)).toFixed(1)
|
|
692
753
|
: "0",
|
|
693
754
|
},
|
|
755
|
+
errors,
|
|
756
|
+
// partial: some venue read failed — portfolio / costs cover only the venues that answered.
|
|
757
|
+
partial: Object.keys(errors).length > 0,
|
|
758
|
+
venuesRead,
|
|
759
|
+
venuesRequested: exchanges.length,
|
|
694
760
|
};
|
|
695
761
|
if (isJson())
|
|
696
762
|
return printJson(jsonOk(report));
|
|
@@ -707,6 +773,8 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
|
|
|
707
773
|
console.log(` Trading Fees: ${chalk.red(`-$${formatUsd(totalFees)}`)}`);
|
|
708
774
|
console.log(` Funding Income: ${formatPnl(totalFunding)}`);
|
|
709
775
|
console.log(` Net: ${formatPnl(totalFunding - totalFees)}`);
|
|
776
|
+
for (const [k, msg] of Object.entries(errors))
|
|
777
|
+
console.log(chalk.yellow(` ⚠ ${k} unavailable (left out of the totals): ${msg}`));
|
|
710
778
|
console.log();
|
|
711
779
|
});
|
|
712
780
|
});
|
|
@@ -738,25 +806,49 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
|
|
|
738
806
|
// Fetch funding + current positions from all relevant exchanges
|
|
739
807
|
const fundingByExSym = new Map();
|
|
740
808
|
const currentPrices = new Map();
|
|
809
|
+
// A venue whose adapter / funding / positions read failed is keyed here: rows touching it
|
|
810
|
+
// report null for the unknown part (funding / pricePnl), never 0 or the entry price.
|
|
811
|
+
const errors = {};
|
|
812
|
+
const errText = (e) => (e instanceof Error ? e.message : String(e));
|
|
813
|
+
const fundingFailed = new Set();
|
|
741
814
|
await Promise.allSettled([...exchangeSet].map(async (ex) => {
|
|
815
|
+
let adapter;
|
|
742
816
|
try {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
817
|
+
adapter = await getAdapterForExchange(ex);
|
|
818
|
+
}
|
|
819
|
+
catch (e) {
|
|
820
|
+
errors[`${ex}.adapter`] = errText(e);
|
|
821
|
+
fundingFailed.add(ex);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
const [funding, livePositions] = await Promise.allSettled([
|
|
825
|
+
adapter.getFundingPayments(limit),
|
|
826
|
+
adapter.getPositions(),
|
|
827
|
+
]);
|
|
828
|
+
if (funding.status === "fulfilled") {
|
|
829
|
+
for (const f of funding.value) {
|
|
749
830
|
if (sinceMs && f.time < sinceMs)
|
|
750
831
|
continue;
|
|
751
832
|
const key = `${ex}:${f.symbol}`;
|
|
752
833
|
fundingByExSym.set(key, (fundingByExSym.get(key) ?? 0) + Number(f.payment));
|
|
753
834
|
}
|
|
754
|
-
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
errors[`${ex}.funding`] = errText(funding.reason);
|
|
838
|
+
fundingFailed.add(ex);
|
|
839
|
+
}
|
|
840
|
+
if (livePositions.status === "fulfilled") {
|
|
841
|
+
for (const p of livePositions.value) {
|
|
755
842
|
currentPrices.set(`${ex}:${p.symbol}`, Number(p.markPrice));
|
|
756
843
|
}
|
|
757
844
|
}
|
|
758
|
-
|
|
845
|
+
else
|
|
846
|
+
errors[`${ex}.positions`] = errText(livePositions.reason);
|
|
759
847
|
}));
|
|
848
|
+
// Funding of a venue that answered: the sum of its payments (none = 0). A failed venue: unknown (null).
|
|
849
|
+
const fundingOf = (ex, sym) => fundingFailed.has(ex) ? null : fundingByExSym.get(`${ex}:${sym}`) ?? 0;
|
|
850
|
+
// Current mark from the live position; none (failed read / no live leg) = unknown (null), never the entry price.
|
|
851
|
+
const priceOf = (ex, sym) => currentPrices.get(`${ex}:${sym}`) ?? null;
|
|
760
852
|
// Entries without a verified entry price (a bot leg the venue reported no fill price for)
|
|
761
853
|
// have no price PnL: listed apart, never priced at a guess.
|
|
762
854
|
const priced = positions.filter((p) => p.entryLongPrice !== null && p.entryShortPrice !== null);
|
|
@@ -764,21 +856,21 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
|
|
|
764
856
|
// Build comparison data
|
|
765
857
|
const rows = priced.map(pos => {
|
|
766
858
|
const daysHeld = Math.max(1, Math.round((Date.now() - new Date(pos.entryTime).getTime()) / 86400000));
|
|
767
|
-
const longFunding =
|
|
768
|
-
const shortFunding =
|
|
769
|
-
const netFunding = longFunding + shortFunding;
|
|
770
|
-
// Estimate unrealized PnL from price movement
|
|
771
|
-
const longPrice =
|
|
772
|
-
const shortPrice =
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
const totalPnl = pricePnl + netFunding;
|
|
859
|
+
const longFunding = fundingOf(pos.longExchange, pos.symbol);
|
|
860
|
+
const shortFunding = fundingOf(pos.shortExchange, pos.symbol);
|
|
861
|
+
const netFunding = longFunding !== null && shortFunding !== null ? longFunding + shortFunding : null;
|
|
862
|
+
// Estimate unrealized PnL from price movement (null when either leg's mark is unknown)
|
|
863
|
+
const longPrice = priceOf(pos.longExchange, pos.symbol);
|
|
864
|
+
const shortPrice = priceOf(pos.shortExchange, pos.symbol);
|
|
865
|
+
const pricePnl = longPrice !== null && shortPrice !== null
|
|
866
|
+
? (longPrice - pos.entryLongPrice) * pos.longSize + (pos.entryShortPrice - shortPrice) * pos.shortSize
|
|
867
|
+
: null;
|
|
868
|
+
const totalPnl = pricePnl !== null && netFunding !== null ? pricePnl + netFunding : null;
|
|
777
869
|
// Notional for ROI calculation
|
|
778
870
|
const notional = pos.entryLongPrice * pos.longSize + pos.entryShortPrice * pos.shortSize;
|
|
779
|
-
const roi = notional > 0 ? (totalPnl / notional) * 100 : 0;
|
|
780
|
-
const fundingRoi = notional > 0 ? (netFunding / notional) * 100 : 0;
|
|
781
|
-
const annualizedRoi = daysHeld > 0 ? (fundingRoi / daysHeld) * 365 : 0;
|
|
871
|
+
const roi = totalPnl !== null ? (notional > 0 ? (totalPnl / notional) * 100 : 0) : null;
|
|
872
|
+
const fundingRoi = netFunding !== null ? (notional > 0 ? (netFunding / notional) * 100 : 0) : null;
|
|
873
|
+
const annualizedRoi = fundingRoi !== null ? (daysHeld > 0 ? (fundingRoi / daysHeld) * 365 : 0) : null;
|
|
782
874
|
return {
|
|
783
875
|
symbol: pos.symbol,
|
|
784
876
|
mode: pos.mode ?? "perp-perp",
|
|
@@ -795,14 +887,19 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
|
|
|
795
887
|
annualizedRoi,
|
|
796
888
|
};
|
|
797
889
|
});
|
|
798
|
-
// Sort by total PnL descending
|
|
799
|
-
rows.sort((a, b) => b.totalPnl - a.totalPnl);
|
|
890
|
+
// Sort by total PnL descending (unknown totals last)
|
|
891
|
+
rows.sort((a, b) => (a.totalPnl === null ? 1 : b.totalPnl === null ? -1 : b.totalPnl - a.totalPnl));
|
|
892
|
+
// Totals skip the unknown (null) values; partial flags that they do not cover every row.
|
|
893
|
+
const sumKnown = (vals) => vals.reduce((s, v) => s + (v ?? 0), 0);
|
|
894
|
+
const complete = rows.filter(r => r.totalPnl !== null);
|
|
800
895
|
const totals = {
|
|
801
|
-
funding: rows.
|
|
802
|
-
pricePnl: rows.
|
|
803
|
-
totalPnl:
|
|
804
|
-
|
|
896
|
+
funding: sumKnown(rows.map(r => r.funding)),
|
|
897
|
+
pricePnl: sumKnown(rows.map(r => r.pricePnl)),
|
|
898
|
+
totalPnl: sumKnown(complete.map(r => r.totalPnl)),
|
|
899
|
+
// ROI base: only the rows whose total PnL is known.
|
|
900
|
+
notional: complete.reduce((s, r) => s + r.notional, 0),
|
|
805
901
|
};
|
|
902
|
+
const partial = Object.keys(errors).length > 0 || rows.some(r => r.funding === null || r.pricePnl === null);
|
|
806
903
|
const result = {
|
|
807
904
|
period: opts.period ?? "all",
|
|
808
905
|
positionCount: rows.length,
|
|
@@ -812,6 +909,8 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
|
|
|
812
909
|
...totals,
|
|
813
910
|
roi: totals.notional > 0 ? (totals.totalPnl / totals.notional) * 100 : 0,
|
|
814
911
|
},
|
|
912
|
+
errors,
|
|
913
|
+
partial,
|
|
815
914
|
};
|
|
816
915
|
if (isJson())
|
|
817
916
|
return printJson(jsonOk(result));
|
|
@@ -822,11 +921,11 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
|
|
|
822
921
|
`${r.longExchange} → ${r.shortExchange}`,
|
|
823
922
|
r.mode,
|
|
824
923
|
`${r.daysHeld}d`,
|
|
825
|
-
formatPnl(r.funding),
|
|
826
|
-
formatPnl(r.pricePnl),
|
|
827
|
-
formatPnl(r.totalPnl),
|
|
828
|
-
`${r.roi >= 0 ? "+" : ""}${r.roi.toFixed(2)}
|
|
829
|
-
`${r.annualizedRoi >= 0 ? "+" : ""}${r.annualizedRoi.toFixed(1)}
|
|
924
|
+
r.funding !== null ? formatPnl(r.funding) : chalk.yellow("unavailable"),
|
|
925
|
+
r.pricePnl !== null ? formatPnl(r.pricePnl) : chalk.yellow("unavailable"),
|
|
926
|
+
r.totalPnl !== null ? formatPnl(r.totalPnl) : chalk.yellow("unavailable"),
|
|
927
|
+
r.roi !== null ? `${r.roi >= 0 ? "+" : ""}${r.roi.toFixed(2)}%` : chalk.yellow("unavailable"),
|
|
928
|
+
r.annualizedRoi !== null ? `${r.annualizedRoi >= 0 ? "+" : ""}${r.annualizedRoi.toFixed(1)}%` : chalk.yellow("unavailable"),
|
|
830
929
|
]);
|
|
831
930
|
console.log(makeTable(["Symbol", "Long → Short", "Mode", "Held", "Funding", "Price PnL", "Total PnL", "ROI", "Ann.ROI"], tableRows));
|
|
832
931
|
if (unpricedPositions.length > 0)
|
|
@@ -838,6 +937,10 @@ function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
|
|
|
838
937
|
if (totals.notional > 0) {
|
|
839
938
|
console.log(` Total ROI: ${result.totals.roi >= 0 ? "+" : ""}${result.totals.roi.toFixed(2)}%`);
|
|
840
939
|
}
|
|
940
|
+
if (partial)
|
|
941
|
+
console.log(chalk.yellow(" ⚠ Partial: totals leave out the unavailable values above"));
|
|
942
|
+
for (const [k, msg] of Object.entries(errors))
|
|
943
|
+
console.log(chalk.yellow(` ⚠ ${k} unavailable: ${msg}`));
|
|
841
944
|
console.log();
|
|
842
945
|
});
|
|
843
946
|
});
|
|
@@ -17,4 +17,9 @@
|
|
|
17
17
|
import type { Command } from "commander";
|
|
18
18
|
import type { ExchangeAdapter } from "../exchanges/index.js";
|
|
19
19
|
import "../exchanges/venues/index.js";
|
|
20
|
+
/**
|
|
21
|
+
* Open-position count on a HIP-3 dex; null = the read failed (unknown — NOT "no positions").
|
|
22
|
+
* The failure is warned on stderr; callers must treat null as "may hold positions".
|
|
23
|
+
*/
|
|
24
|
+
export declare function checkDexPositions(address: string, dex: string): Promise<number | null>;
|
|
20
25
|
export declare function registerPortfolioCommand(program: Command, getAdapterForExchange: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean, version: string, getHLAdapterForDex?: (dex: string) => Promise<ExchangeAdapter>): void;
|
|
@@ -176,7 +176,7 @@ async function fetchExchangeEntry(exName, getAdapter) {
|
|
|
176
176
|
adapter.getBalance(),
|
|
177
177
|
adapter.getPositions(),
|
|
178
178
|
adapter.getOpenOrders(),
|
|
179
|
-
adapter.getFundingPayments(200).
|
|
179
|
+
adapter.getFundingPayments(200).then((rows) => ({ rows, error: null }), (e) => ({ rows: null, error: e instanceof Error ? e.message : String(e) })),
|
|
180
180
|
]);
|
|
181
181
|
// Spot balances for every venue with def.spot (§12). Failures are surfaced on
|
|
182
182
|
// stderr (the perp snapshot is still reported); unpriced tokens are excluded + warned.
|
|
@@ -200,8 +200,11 @@ async function fetchExchangeEntry(exName, getAdapter) {
|
|
|
200
200
|
warn(`${def.displayName} spot balances unavailable: ${e instanceof Error ? e.message : String(e)}`);
|
|
201
201
|
}
|
|
202
202
|
const cutoff24h = Date.now() - 24 * 60 * 60 * 1000;
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
if (fundingPayments.error !== null)
|
|
204
|
+
warn(`${def.displayName} funding history unavailable (funding24h null): ${fundingPayments.error}`);
|
|
205
|
+
const funding24h = fundingPayments.rows === null
|
|
206
|
+
? null
|
|
207
|
+
: fundingPayments.rows.filter(f => f.time >= cutoff24h).reduce((sum, f) => sum + Number(f.payment), 0);
|
|
205
208
|
// Shared balances (Pacifica USDC, HL unified USDC) are already in perp equity (§12.4 #8).
|
|
206
209
|
const nonUsdcSpotValue = spot.filter(b => !b.shared).reduce((s, b) => s + b.valueUsd, 0);
|
|
207
210
|
return {
|
|
@@ -227,6 +230,7 @@ async function fetchExchangeEntry(exName, getAdapter) {
|
|
|
227
230
|
})),
|
|
228
231
|
openOrders: orders.length,
|
|
229
232
|
funding24h,
|
|
233
|
+
fundingError: fundingPayments.error,
|
|
230
234
|
totalAccountValueUsd: Number(bal.equity) + nonUsdcSpotValue,
|
|
231
235
|
error: null,
|
|
232
236
|
};
|
|
@@ -240,7 +244,9 @@ async function fetchExchangeEntry(exName, getAdapter) {
|
|
|
240
244
|
spot: [],
|
|
241
245
|
positions: [],
|
|
242
246
|
openOrders: 0,
|
|
243
|
-
funding24h:
|
|
247
|
+
funding24h: null,
|
|
248
|
+
// The whole venue was unreadable: funding is unknown for the same reason.
|
|
249
|
+
fundingError: err instanceof Error ? err.message : String(err),
|
|
244
250
|
totalAccountValueUsd: 0,
|
|
245
251
|
error: err instanceof Error ? err.message : String(err),
|
|
246
252
|
};
|
|
@@ -251,13 +257,17 @@ function buildTotals(entries) {
|
|
|
251
257
|
let spotValueUsd = 0;
|
|
252
258
|
let marginUsed = 0;
|
|
253
259
|
let funding24h = 0;
|
|
260
|
+
let fundingIncomplete = false;
|
|
254
261
|
let accountValueUsd = 0;
|
|
255
262
|
for (const e of entries) {
|
|
256
263
|
if (e.perp) {
|
|
257
264
|
equity += Number(e.perp.equity);
|
|
258
265
|
marginUsed += Number(e.perp.marginUsed);
|
|
259
266
|
}
|
|
260
|
-
|
|
267
|
+
if (e.funding24h === null)
|
|
268
|
+
fundingIncomplete = true;
|
|
269
|
+
else
|
|
270
|
+
funding24h += e.funding24h;
|
|
261
271
|
// Non-USDC spot value (for unified accounts USDC is already in equity)
|
|
262
272
|
const exSpot = e.isUnified
|
|
263
273
|
? e.spot.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
|
|
@@ -272,7 +282,7 @@ function buildTotals(entries) {
|
|
|
272
282
|
const marginPct = denom > 0 ? (marginUsed / denom) * 100 : 0;
|
|
273
283
|
const level = marginPct < 30 ? "LOW" : marginPct < 60 ? "MEDIUM" : "HIGH";
|
|
274
284
|
return {
|
|
275
|
-
totals: { equity, spotValueUsd, accountValueUsd, marginUsed, marginPct, funding24h },
|
|
285
|
+
totals: { equity, spotValueUsd, accountValueUsd, marginUsed, marginPct, funding24h, fundingIncomplete },
|
|
276
286
|
risk: { level, marginPct },
|
|
277
287
|
};
|
|
278
288
|
}
|
|
@@ -537,7 +547,11 @@ async function runWatch(resolved, version, getAdapterForExchange, isJsonMode) {
|
|
|
537
547
|
}
|
|
538
548
|
// ── Serve mode (delegates to dashboard infra) ──────────────────────────────
|
|
539
549
|
const KNOWN_HIP3_DEXES = ["xyz", "flx", "hyna", "km", "cash", "vntl"];
|
|
540
|
-
|
|
550
|
+
/**
|
|
551
|
+
* Open-position count on a HIP-3 dex; null = the read failed (unknown — NOT "no positions").
|
|
552
|
+
* The failure is warned on stderr; callers must treat null as "may hold positions".
|
|
553
|
+
*/
|
|
554
|
+
export async function checkDexPositions(address, dex) {
|
|
541
555
|
try {
|
|
542
556
|
// Selected network (review L7): follows -n via setSharedApiNetwork (live binding).
|
|
543
557
|
const res = await fetch(HYPERLIQUID_API_URL, {
|
|
@@ -545,15 +559,22 @@ async function checkDexPositions(address, dex) {
|
|
|
545
559
|
headers: { "Content-Type": "application/json" },
|
|
546
560
|
body: JSON.stringify({ type: "clearinghouseState", user: address, dex }),
|
|
547
561
|
});
|
|
562
|
+
if (!res.ok)
|
|
563
|
+
throw new Error(`HTTP ${res.status}`);
|
|
548
564
|
const state = await res.json();
|
|
549
|
-
|
|
550
|
-
|
|
565
|
+
if (!state || !Array.isArray(state.assetPositions))
|
|
566
|
+
throw new Error("clearinghouseState without assetPositions");
|
|
567
|
+
return state.assetPositions.filter(p => {
|
|
551
568
|
const pos = (p.position ?? p);
|
|
552
|
-
|
|
569
|
+
const szi = Number(pos.szi);
|
|
570
|
+
if (!Number.isFinite(szi))
|
|
571
|
+
throw new Error(`position without a numeric szi (${String(pos.coin)})`);
|
|
572
|
+
return szi !== 0;
|
|
553
573
|
}).length;
|
|
554
574
|
}
|
|
555
|
-
catch {
|
|
556
|
-
|
|
575
|
+
catch (e) {
|
|
576
|
+
process.stderr.write(`[warn] hl:${dex} positions unknown (HIP-3 dex read failed; included in the dashboard): ${e instanceof Error ? e.message : String(e)}\n`);
|
|
577
|
+
return null;
|
|
557
578
|
}
|
|
558
579
|
}
|
|
559
580
|
async function autoDetectHip3Dexes(address, getHLAdapterForDex, verbose) {
|
|
@@ -561,16 +582,22 @@ async function autoDetectHip3Dexes(address, getHLAdapterForDex, verbose) {
|
|
|
561
582
|
dex,
|
|
562
583
|
count: await checkDexPositions(address, dex),
|
|
563
584
|
})));
|
|
564
|
-
|
|
585
|
+
// Unknown (null) counts are included: a failed read must not hide a dex that may hold positions.
|
|
586
|
+
const activeDexes = checks.filter(c => c.count === null || c.count > 0);
|
|
565
587
|
const results = [];
|
|
566
588
|
for (const { dex, count } of activeDexes) {
|
|
567
589
|
try {
|
|
568
590
|
const adapter = await getHLAdapterForDex(dex);
|
|
569
591
|
results.push({ name: `hl:${dex}`, adapter });
|
|
570
|
-
if (verbose)
|
|
571
|
-
console.log(
|
|
592
|
+
if (verbose) {
|
|
593
|
+
console.log(count === null
|
|
594
|
+
? chalk.yellow(` ⚠ hl:${dex} (HIP-3, positions unknown — read failed)`)
|
|
595
|
+
: chalk.green(` ✓ hl:${dex} (HIP-3, ${count} position${count > 1 ? "s" : ""})`));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
catch (e) {
|
|
599
|
+
process.stderr.write(`[warn] hl:${dex} left out of the dashboard (adapter unavailable${count === null ? ", positions unknown" : `, ${count} position(s)`}): ${e instanceof Error ? e.message : String(e)}\n`);
|
|
572
600
|
}
|
|
573
|
-
catch { /* skip */ }
|
|
574
601
|
}
|
|
575
602
|
return results;
|
|
576
603
|
}
|
|
@@ -578,12 +605,20 @@ async function runServe(resolved, getAdapterForExchange, isJsonMode, getHLAdapte
|
|
|
578
605
|
const { startDashboard } = await import("../dashboard/index.js");
|
|
579
606
|
if (isJsonMode) {
|
|
580
607
|
const exchanges = [];
|
|
608
|
+
// A venue whose adapter failed is not monitored — reported in serve.errors + stderr, not dropped silently.
|
|
609
|
+
const errors = {};
|
|
610
|
+
const noteError = (name, e) => {
|
|
611
|
+
errors[name] = e instanceof Error ? e.message : String(e);
|
|
612
|
+
process.stderr.write(`[warn] ${name} not monitored (adapter unavailable): ${errors[name]}\n`);
|
|
613
|
+
};
|
|
581
614
|
for (const name of resolved.exchangeList) {
|
|
582
615
|
try {
|
|
583
616
|
const adapter = await getAdapterForExchange(name);
|
|
584
617
|
exchanges.push({ name, adapter });
|
|
585
618
|
}
|
|
586
|
-
catch {
|
|
619
|
+
catch (e) {
|
|
620
|
+
noteError(name, e);
|
|
621
|
+
}
|
|
587
622
|
}
|
|
588
623
|
if (getHLAdapterForDex) {
|
|
589
624
|
for (const dex of resolved.dexList) {
|
|
@@ -591,7 +626,9 @@ async function runServe(resolved, getAdapterForExchange, isJsonMode, getHLAdapte
|
|
|
591
626
|
const adapter = await getHLAdapterForDex(dex);
|
|
592
627
|
exchanges.push({ name: `hl:${dex}`, adapter });
|
|
593
628
|
}
|
|
594
|
-
catch {
|
|
629
|
+
catch (e) {
|
|
630
|
+
noteError(`hl:${dex}`, e);
|
|
631
|
+
}
|
|
595
632
|
}
|
|
596
633
|
if (resolved.autoDex && !resolved.dexList.length && resolved.exchangeList.includes("hyperliquid")) {
|
|
597
634
|
const hlEx = exchanges.find(e => e.name === "hyperliquid");
|
|
@@ -612,12 +649,13 @@ async function runServe(resolved, getAdapterForExchange, isJsonMode, getHLAdapte
|
|
|
612
649
|
scope: resolved.scope,
|
|
613
650
|
mode: "serve",
|
|
614
651
|
exchanges: [],
|
|
615
|
-
totals: { equity: 0, spotValueUsd: 0, accountValueUsd: 0, marginUsed: 0, marginPct: 0, funding24h: 0 },
|
|
652
|
+
totals: { equity: 0, spotValueUsd: 0, accountValueUsd: 0, marginUsed: 0, marginPct: 0, funding24h: 0, fundingIncomplete: false },
|
|
616
653
|
risk: { level: "LOW", marginPct: 0 },
|
|
617
654
|
serve: {
|
|
618
655
|
url: `http://localhost:${dashboard.port}`,
|
|
619
656
|
port: dashboard.port,
|
|
620
657
|
exchanges: exchanges.map(e => e.name),
|
|
658
|
+
errors,
|
|
621
659
|
},
|
|
622
660
|
};
|
|
623
661
|
printJson(jsonOk(data));
|
|
@@ -757,7 +795,7 @@ export function registerPortfolioCommand(program, getAdapterForExchange, isJson,
|
|
|
757
795
|
scope: resolved.scope,
|
|
758
796
|
mode: "health",
|
|
759
797
|
exchanges: [],
|
|
760
|
-
totals: { equity: 0, spotValueUsd: 0, accountValueUsd: 0, marginUsed: 0, marginPct: 0, funding24h: 0 },
|
|
798
|
+
totals: { equity: 0, spotValueUsd: 0, accountValueUsd: 0, marginUsed: 0, marginPct: 0, funding24h: 0, fundingIncomplete: false },
|
|
761
799
|
risk: { level: "LOW", marginPct: 0 },
|
|
762
800
|
health,
|
|
763
801
|
};
|