perp-cli 0.4.12 → 0.5.0
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/commands/account.js +151 -20
- package/dist/commands/agent.js +5 -5
- package/dist/commands/arb/index.js +191 -24
- package/dist/commands/arb-auto.d.ts +9 -0
- package/dist/commands/arb-auto.js +577 -382
- package/dist/commands/arb.d.ts +44 -0
- package/dist/commands/arb.js +236 -514
- package/dist/commands/market.js +108 -73
- package/dist/exchanges/hyperliquid.d.ts +2 -2
- package/dist/exchanges/hyperliquid.js +126 -31
- package/dist/exchanges/lighter.js +21 -7
- package/dist/liquidity.js +3 -1
- package/package.json +1 -1
package/dist/commands/arb.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { makeTable, formatUsd, formatPercent, formatPnl, printJson, jsonOk,
|
|
1
|
+
import { makeTable, formatUsd, formatPercent, formatPnl, printJson, jsonOk, withJsonErrors } from "../utils.js";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { annualizeRate, toHourlyRate, estimateHourlyFunding, annualizeHourlyRate } from "../funding.js";
|
|
4
4
|
import { fetchAllFundingRates, fetchSymbolFundingRates, TOP_SYMBOLS, } from "../funding-rates.js";
|
|
@@ -53,183 +53,10 @@ async function fetchLighterRates() {
|
|
|
53
53
|
// annualize moved to ../funding.ts — use annualizeRate() / computeAnnualSpread()
|
|
54
54
|
export function registerArbCommands(program, isJson, getAdapterForExchange) {
|
|
55
55
|
const arb = program.command("arb").description("Funding rate arbitrage & basis trading");
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
fundingCmd._hidden = true;
|
|
59
|
-
fundingCmd.action(() => console.log("Use 'perp arb rates --min-spread 5' instead."));
|
|
60
|
-
arb
|
|
61
|
-
.command("basis")
|
|
62
|
-
.description("Show basis (spot vs perp price) opportunities across exchanges")
|
|
63
|
-
.option("--min-basis <pct>", "Minimum annual basis % to show", "3")
|
|
64
|
-
.action(async (opts) => {
|
|
65
|
-
if (!isJson())
|
|
66
|
-
console.log(chalk.cyan(" Fetching prices for basis calculation...\n"));
|
|
67
|
-
const [pacRates, hlRates, ltRates] = await Promise.all([
|
|
68
|
-
fetchPacificaRates(),
|
|
69
|
-
fetchHyperliquidRates(),
|
|
70
|
-
fetchLighterRates(),
|
|
71
|
-
]);
|
|
72
|
-
// Compare mark prices across all exchanges for cross-exchange basis
|
|
73
|
-
const exchangePrices = new Map();
|
|
74
|
-
for (const r of [...pacRates, ...hlRates, ...ltRates]) {
|
|
75
|
-
if (r.markPrice <= 0)
|
|
76
|
-
continue;
|
|
77
|
-
if (!exchangePrices.has(r.symbol))
|
|
78
|
-
exchangePrices.set(r.symbol, new Map());
|
|
79
|
-
exchangePrices.get(r.symbol).set(r.exchange, r.markPrice);
|
|
80
|
-
}
|
|
81
|
-
const opps = [];
|
|
82
|
-
for (const [symbol, prices] of exchangePrices.entries()) {
|
|
83
|
-
if (prices.size < 2)
|
|
84
|
-
continue;
|
|
85
|
-
// Find cheapest and most expensive exchange
|
|
86
|
-
let minEx = "", maxEx = "", minPrice = Infinity, maxPrice = 0;
|
|
87
|
-
for (const [ex, price] of prices) {
|
|
88
|
-
if (price < minPrice) {
|
|
89
|
-
minPrice = price;
|
|
90
|
-
minEx = ex;
|
|
91
|
-
}
|
|
92
|
-
if (price > maxPrice) {
|
|
93
|
-
maxPrice = price;
|
|
94
|
-
maxEx = ex;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if (minEx === maxEx)
|
|
98
|
-
continue;
|
|
99
|
-
const diff = maxPrice - minPrice;
|
|
100
|
-
const avgPrice = (maxPrice + minPrice) / 2;
|
|
101
|
-
const pctDiff = (diff / avgPrice) * 100;
|
|
102
|
-
if (pctDiff * 365 >= parseFloat(opts.minBasis)) {
|
|
103
|
-
const priceObj = {};
|
|
104
|
-
for (const [ex, p] of prices)
|
|
105
|
-
priceObj[ex] = p;
|
|
106
|
-
opps.push({
|
|
107
|
-
symbol,
|
|
108
|
-
prices: priceObj,
|
|
109
|
-
priceDiff: diff,
|
|
110
|
-
pctDiff,
|
|
111
|
-
buyOn: minEx,
|
|
112
|
-
sellOn: maxEx,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
opps.sort((a, b) => b.pctDiff - a.pctDiff);
|
|
117
|
-
if (isJson())
|
|
118
|
-
return printJson(jsonOk(opps));
|
|
119
|
-
if (opps.length === 0) {
|
|
120
|
-
console.log(chalk.gray(` No basis opportunities above ${opts.minBasis}% threshold.\n`));
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
console.log(chalk.cyan.bold(" Cross-Exchange Basis Opportunities\n"));
|
|
124
|
-
console.log(chalk.gray(" Strategy: Buy cheap, sell expensive, profit from convergence.\n"));
|
|
125
|
-
const rows = opps.map((o) => [
|
|
126
|
-
chalk.white.bold(o.symbol),
|
|
127
|
-
o.prices.pacifica ? `$${formatUsd(o.prices.pacifica)}` : chalk.gray("-"),
|
|
128
|
-
o.prices.hyperliquid ? `$${formatUsd(o.prices.hyperliquid)}` : chalk.gray("-"),
|
|
129
|
-
o.prices.lighter ? `$${formatUsd(o.prices.lighter)}` : chalk.gray("-"),
|
|
130
|
-
`$${formatUsd(o.priceDiff)}`,
|
|
131
|
-
`${o.pctDiff.toFixed(4)}%`,
|
|
132
|
-
chalk.green(o.buyOn),
|
|
133
|
-
chalk.red(o.sellOn),
|
|
134
|
-
]);
|
|
135
|
-
console.log(makeTable(["Symbol", "Pacifica", "Hyperliquid", "Lighter", "Diff", "Basis %", "Buy On", "Sell On"], rows));
|
|
136
|
-
});
|
|
137
|
-
// ── arb dex ── (HIP-3 cross-dex funding arb scan)
|
|
138
|
-
arb
|
|
139
|
-
.command("dex")
|
|
140
|
-
.description("Scan HIP-3 cross-dex funding arb opportunities on Hyperliquid")
|
|
141
|
-
.option("--min-spread <pct>", "Min annual spread % to show", "5")
|
|
142
|
-
.option("--max-gap <pct>", "Max price gap % to treat as same asset", "5")
|
|
143
|
-
.option("--include-native", "Include native HL perps in comparison", true)
|
|
144
|
-
.option("--no-include-native", "Exclude native HL perps")
|
|
145
|
-
.option("--top <n>", "Show top N opportunities", "30")
|
|
146
|
-
.action(async (opts) => {
|
|
147
|
-
if (!isJson())
|
|
148
|
-
console.log(chalk.cyan(" Scanning HIP-3 deployed dex funding spreads...\n"));
|
|
149
|
-
const pairs = await scanDexArb({
|
|
150
|
-
minAnnualSpread: parseFloat(opts.minSpread),
|
|
151
|
-
maxPriceGapPct: parseFloat(opts.maxGap),
|
|
152
|
-
includeNative: opts.includeNative,
|
|
153
|
-
});
|
|
154
|
-
const topN = parseInt(opts.top);
|
|
155
|
-
const shown = pairs.slice(0, topN);
|
|
156
|
-
if (isJson())
|
|
157
|
-
return printJson(jsonOk(shown.map(p => ({
|
|
158
|
-
underlying: p.underlying,
|
|
159
|
-
longDex: p.long.dex,
|
|
160
|
-
longSymbol: p.long.raw,
|
|
161
|
-
longFunding: p.long.fundingRate,
|
|
162
|
-
longPrice: p.long.markPrice,
|
|
163
|
-
longOiUsd: p.long.openInterest * p.long.markPrice,
|
|
164
|
-
longVolume24h: p.long.volume24h,
|
|
165
|
-
shortDex: p.short.dex,
|
|
166
|
-
shortSymbol: p.short.raw,
|
|
167
|
-
shortFunding: p.short.fundingRate,
|
|
168
|
-
shortPrice: p.short.markPrice,
|
|
169
|
-
shortOiUsd: p.short.openInterest * p.short.markPrice,
|
|
170
|
-
shortVolume24h: p.short.volume24h,
|
|
171
|
-
annualSpread: p.annualSpread,
|
|
172
|
-
priceGapPct: p.priceGapPct,
|
|
173
|
-
minOiUsd: p.minOiUsd,
|
|
174
|
-
minVolume24hUsd: p.minVolume24hUsd,
|
|
175
|
-
viability: p.viability,
|
|
176
|
-
}))));
|
|
177
|
-
if (shown.length === 0) {
|
|
178
|
-
console.log(chalk.gray(` No cross-dex opportunities above ${opts.minSpread}% annual spread.\n`));
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
console.log(chalk.cyan.bold(" HIP-3 Cross-Dex Funding Arb Opportunities\n"));
|
|
182
|
-
console.log(chalk.gray(" Strategy: Long on low-funding dex, Short on high-funding dex (same underlying)\n"));
|
|
183
|
-
const rows = shown.map((p) => {
|
|
184
|
-
const spreadColor = p.annualSpread >= 50 ? chalk.green.bold
|
|
185
|
-
: p.annualSpread >= 20 ? chalk.green
|
|
186
|
-
: chalk.yellow;
|
|
187
|
-
const viabilityColor = p.viability === "A" ? chalk.green.bold
|
|
188
|
-
: p.viability === "B" ? chalk.green
|
|
189
|
-
: p.viability === "C" ? chalk.yellow
|
|
190
|
-
: chalk.red;
|
|
191
|
-
const fmtOi = p.minOiUsd >= 1_000_000 ? `$${(p.minOiUsd / 1_000_000).toFixed(1)}M`
|
|
192
|
-
: p.minOiUsd >= 1_000 ? `$${(p.minOiUsd / 1_000).toFixed(0)}K`
|
|
193
|
-
: `$${p.minOiUsd.toFixed(0)}`;
|
|
194
|
-
const fmtVol = p.minVolume24hUsd >= 1_000_000 ? `$${(p.minVolume24hUsd / 1_000_000).toFixed(1)}M`
|
|
195
|
-
: p.minVolume24hUsd >= 1_000 ? `$${(p.minVolume24hUsd / 1_000).toFixed(0)}K`
|
|
196
|
-
: `$${p.minVolume24hUsd.toFixed(0)}`;
|
|
197
|
-
return [
|
|
198
|
-
chalk.white.bold(p.underlying),
|
|
199
|
-
`${p.long.dex}:${p.long.base}`,
|
|
200
|
-
formatPercent(p.long.fundingRate),
|
|
201
|
-
`${p.short.dex}:${p.short.base}`,
|
|
202
|
-
formatPercent(p.short.fundingRate),
|
|
203
|
-
`$${formatUsd(p.long.markPrice)}`,
|
|
204
|
-
spreadColor(`${p.annualSpread.toFixed(1)}%`),
|
|
205
|
-
p.priceGapPct < 0.1 ? chalk.green(`${p.priceGapPct.toFixed(3)}%`) : chalk.yellow(`${p.priceGapPct.toFixed(3)}%`),
|
|
206
|
-
viabilityColor(p.viability),
|
|
207
|
-
viabilityColor(fmtOi),
|
|
208
|
-
chalk.gray(fmtVol),
|
|
209
|
-
];
|
|
210
|
-
});
|
|
211
|
-
console.log(makeTable(["Asset", "Long On", "L.Fund", "Short On", "S.Fund", "Price", "Ann.Spread", "Gap", "Grade", "MinOI", "MinVol"], rows));
|
|
212
|
-
// Summary
|
|
213
|
-
const dexes = new Set();
|
|
214
|
-
for (const p of shown) {
|
|
215
|
-
dexes.add(p.long.dex);
|
|
216
|
-
dexes.add(p.short.dex);
|
|
217
|
-
}
|
|
218
|
-
const gradeCount = { A: 0, B: 0, C: 0, D: 0 };
|
|
219
|
-
for (const p of shown)
|
|
220
|
-
gradeCount[p.viability]++;
|
|
221
|
-
console.log(chalk.gray(`\n ${shown.length}/${pairs.length} opportunities | Dexes: ${[...dexes].sort().join(", ")}`));
|
|
222
|
-
console.log(chalk.gray(` Grade: ${chalk.green.bold(`A(${gradeCount.A})`)} ${chalk.green(`B(${gradeCount.B})`)} ${chalk.yellow(`C(${gradeCount.C})`)} ${chalk.red(`D(${gradeCount.D})`)}`));
|
|
223
|
-
console.log(chalk.gray(` A: OI>$1M B: OI>$100K C: OI>$10K D: OI<$10K (thin — high risk)`));
|
|
224
|
-
console.log(chalk.gray(` Note: HIP-3 dexes charge 2x fees. Factor into net profitability.\n`));
|
|
225
|
-
});
|
|
226
|
-
// ── arb watch/track/alert ── (promoted from arb gap)
|
|
227
|
-
registerGapDirectCommands(arb, isJson);
|
|
228
|
-
// ── Funding subcommands (merged from funding.ts) ──
|
|
229
|
-
registerFundingSubcommands(arb, isJson, getAdapterForExchange);
|
|
230
|
-
// Deprecated aliases removed — use arb prices/watch/track/alert/rates directly
|
|
56
|
+
// basis, dex, rates, compare, funding-history, funding-positions, gaps, live monitor
|
|
57
|
+
// → consolidated into 'arb scan' (in arb-auto.ts) with routing options
|
|
231
58
|
}
|
|
232
|
-
async function fetchAllPrices() {
|
|
59
|
+
export async function fetchAllPrices() {
|
|
233
60
|
const [pacRes, hlRes, ltRes] = await Promise.all([
|
|
234
61
|
fetchPacificaPricesRaw(),
|
|
235
62
|
fetchHyperliquidAllMidsRaw(),
|
|
@@ -289,14 +116,14 @@ async function fetchAllPrices() {
|
|
|
289
116
|
}
|
|
290
117
|
return snapshots.sort((a, b) => b.maxGapPct - a.maxGapPct);
|
|
291
118
|
}
|
|
292
|
-
function formatGapPrice(p) {
|
|
119
|
+
export function formatGapPrice(p) {
|
|
293
120
|
if (p >= 1000)
|
|
294
121
|
return p.toFixed(2);
|
|
295
122
|
if (p >= 1)
|
|
296
123
|
return p.toFixed(4);
|
|
297
124
|
return p.toFixed(6);
|
|
298
125
|
}
|
|
299
|
-
function printGapTable(snapshots, minGap) {
|
|
126
|
+
export function printGapTable(snapshots, minGap) {
|
|
300
127
|
const filtered = snapshots.filter((s) => s.maxGapPct >= minGap);
|
|
301
128
|
if (filtered.length === 0) {
|
|
302
129
|
console.log(chalk.gray(`\n No gaps above ${minGap}%\n`));
|
|
@@ -322,7 +149,7 @@ function printGapTable(snapshots, minGap) {
|
|
|
322
149
|
const top = filtered[0];
|
|
323
150
|
console.log(chalk.gray(`\n ${filtered.length} pairs | Avg gap: ${avgGap.toFixed(4)}% | Max: ${top.symbol} ${top.maxGapPct.toFixed(4)}%\n`));
|
|
324
151
|
}
|
|
325
|
-
function printTrackSummary(symbol, samples) {
|
|
152
|
+
export function printTrackSummary(symbol, samples) {
|
|
326
153
|
if (samples.length === 0) {
|
|
327
154
|
console.log(chalk.gray("\n No samples collected.\n"));
|
|
328
155
|
return;
|
|
@@ -346,194 +173,6 @@ function printTrackSummary(symbol, samples) {
|
|
|
346
173
|
}
|
|
347
174
|
console.log();
|
|
348
175
|
}
|
|
349
|
-
/**
|
|
350
|
-
* Register gap-related commands directly on the arb parent (promoted from arb gap).
|
|
351
|
-
* - arb prices: snapshot of cross-exchange prices (was arb gap show)
|
|
352
|
-
* - arb watch: live monitoring (was arb gap watch)
|
|
353
|
-
* - arb track: track gap over time (was arb gap track)
|
|
354
|
-
* - arb alert: threshold alert (was arb gap alert)
|
|
355
|
-
*/
|
|
356
|
-
function registerGapDirectCommands(arb, isJson) {
|
|
357
|
-
// ── arb prices (was arb gap show) ──
|
|
358
|
-
arb
|
|
359
|
-
.command("prices")
|
|
360
|
-
.description("Show current prices across all exchanges with gap analysis")
|
|
361
|
-
.option("--min <pct>", "Minimum gap % to display", "0.01")
|
|
362
|
-
.option("--symbol <sym>", "Filter by symbol (e.g. BTC, ETH)")
|
|
363
|
-
.option("--top <n>", "Show top N gaps only")
|
|
364
|
-
.action(async (opts) => {
|
|
365
|
-
const minGap = parseFloat(opts.min);
|
|
366
|
-
if (!isJson())
|
|
367
|
-
console.log(chalk.cyan("\n Fetching prices from Pacifica, Hyperliquid & Lighter...\n"));
|
|
368
|
-
let snapshots = await fetchAllPrices();
|
|
369
|
-
if (opts.symbol) {
|
|
370
|
-
const sym = opts.symbol.toUpperCase();
|
|
371
|
-
snapshots = snapshots.filter((s) => s.symbol.includes(sym));
|
|
372
|
-
}
|
|
373
|
-
if (opts.top) {
|
|
374
|
-
snapshots = snapshots.slice(0, parseInt(opts.top));
|
|
375
|
-
}
|
|
376
|
-
if (isJson())
|
|
377
|
-
return printJson(jsonOk(snapshots.filter((s) => s.maxGapPct >= minGap)));
|
|
378
|
-
printGapTable(snapshots, minGap);
|
|
379
|
-
});
|
|
380
|
-
// ── arb watch (was arb gap watch) ──
|
|
381
|
-
arb
|
|
382
|
-
.command("watch")
|
|
383
|
-
.description("Live-monitor price gaps across exchanges (auto-refresh)")
|
|
384
|
-
.option("--min <pct>", "Minimum gap % to display", "0.05")
|
|
385
|
-
.option("--interval <seconds>", "Refresh interval", "5")
|
|
386
|
-
.option("--symbol <sym>", "Filter by symbol")
|
|
387
|
-
.option("--beep", "Beep on gaps above 0.5%")
|
|
388
|
-
.action(async (opts) => {
|
|
389
|
-
const minGap = parseFloat(opts.min);
|
|
390
|
-
const intervalMs = parseInt(opts.interval) * 1000;
|
|
391
|
-
const beep = !!opts.beep;
|
|
392
|
-
if (!isJson()) {
|
|
393
|
-
console.log(chalk.cyan.bold("\n Price Gap Monitor"));
|
|
394
|
-
console.log(` Min gap: ${minGap}%`);
|
|
395
|
-
console.log(` Interval: ${opts.interval}s`);
|
|
396
|
-
if (opts.symbol)
|
|
397
|
-
console.log(` Symbol: ${opts.symbol.toUpperCase()}`);
|
|
398
|
-
console.log(chalk.gray(" Ctrl+C to stop\n"));
|
|
399
|
-
}
|
|
400
|
-
const cycle = async () => {
|
|
401
|
-
try {
|
|
402
|
-
let snapshots = await fetchAllPrices();
|
|
403
|
-
if (opts.symbol) {
|
|
404
|
-
const sym = opts.symbol.toUpperCase();
|
|
405
|
-
snapshots = snapshots.filter((s) => s.symbol.includes(sym));
|
|
406
|
-
}
|
|
407
|
-
process.stdout.write("\x1B[2J\x1B[0f");
|
|
408
|
-
const now = new Date().toLocaleTimeString();
|
|
409
|
-
console.log(chalk.cyan.bold(` Price Gap Monitor`) +
|
|
410
|
-
chalk.gray(` ${now} (refresh: ${opts.interval}s)\n`));
|
|
411
|
-
printGapTable(snapshots, minGap);
|
|
412
|
-
const bigGaps = snapshots.filter((s) => s.maxGapPct >= 0.5);
|
|
413
|
-
if (bigGaps.length > 0) {
|
|
414
|
-
console.log(chalk.red.bold(` !! ${bigGaps.length} large gap(s): ` +
|
|
415
|
-
bigGaps
|
|
416
|
-
.map((s) => `${s.symbol}(${s.maxGapPct.toFixed(3)}%)`)
|
|
417
|
-
.join(", ")));
|
|
418
|
-
if (beep)
|
|
419
|
-
process.stdout.write("\x07");
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
catch (err) {
|
|
423
|
-
console.error(chalk.gray(` Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
424
|
-
}
|
|
425
|
-
};
|
|
426
|
-
await cycle();
|
|
427
|
-
setInterval(cycle, intervalMs);
|
|
428
|
-
await new Promise(() => { }); // keep alive
|
|
429
|
-
});
|
|
430
|
-
// ── arb track (was arb gap track) ──
|
|
431
|
-
arb
|
|
432
|
-
.command("track")
|
|
433
|
-
.description("Track a symbol's price gap over time and print stats")
|
|
434
|
-
.requiredOption("-s, --symbol <sym>", "Symbol to track (e.g. BTC)")
|
|
435
|
-
.option("--duration <minutes>", "How long to track (minutes)", "10")
|
|
436
|
-
.option("--interval <seconds>", "Sample interval", "10")
|
|
437
|
-
.action(async (opts) => {
|
|
438
|
-
const symbol = opts.symbol.toUpperCase();
|
|
439
|
-
const durationMs = parseInt(opts.duration) * 60 * 1000;
|
|
440
|
-
const intervalMs = parseInt(opts.interval) * 1000;
|
|
441
|
-
const endTime = Date.now() + durationMs;
|
|
442
|
-
const samples = [];
|
|
443
|
-
if (!isJson()) {
|
|
444
|
-
console.log(chalk.cyan.bold(`\n Tracking ${symbol} price gap\n`));
|
|
445
|
-
console.log(` Duration: ${opts.duration} min`);
|
|
446
|
-
console.log(` Interval: ${opts.interval}s`);
|
|
447
|
-
console.log(chalk.gray(" Collecting samples...\n"));
|
|
448
|
-
}
|
|
449
|
-
const sample = async () => {
|
|
450
|
-
const snapshots = await fetchAllPrices();
|
|
451
|
-
const s = snapshots.find((x) => x.symbol === symbol);
|
|
452
|
-
if (!s) {
|
|
453
|
-
console.log(chalk.gray(` ${new Date().toLocaleTimeString()} — ${symbol} not found on both exchanges`));
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
samples.push({
|
|
457
|
-
time: new Date().toLocaleTimeString(),
|
|
458
|
-
gap: s.maxGap,
|
|
459
|
-
gapPct: s.maxGapPct,
|
|
460
|
-
direction: `${s.cheapest}→${s.expensive}`,
|
|
461
|
-
});
|
|
462
|
-
const gapColor = s.maxGapPct >= 0.1 ? chalk.yellow : chalk.gray;
|
|
463
|
-
const parts = [`PAC: ${s.pacPrice !== null ? "$" + formatGapPrice(s.pacPrice) : "-"}`];
|
|
464
|
-
parts.push(`HL: ${s.hlPrice !== null ? "$" + formatGapPrice(s.hlPrice) : "-"}`);
|
|
465
|
-
parts.push(`LT: ${s.ltPrice !== null ? "$" + formatGapPrice(s.ltPrice) : "-"}`);
|
|
466
|
-
console.log(` ${chalk.white(s.symbol.padEnd(6))} ` +
|
|
467
|
-
`${parts.join(" ")} ` +
|
|
468
|
-
`${gapColor(`${s.maxGapPct.toFixed(4)}%`)} ${s.cheapest}→${s.expensive}`);
|
|
469
|
-
};
|
|
470
|
-
await sample();
|
|
471
|
-
const timer = setInterval(async () => {
|
|
472
|
-
if (Date.now() >= endTime) {
|
|
473
|
-
clearInterval(timer);
|
|
474
|
-
printTrackSummary(symbol, samples);
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
await sample();
|
|
478
|
-
}, intervalMs);
|
|
479
|
-
await new Promise((resolve) => {
|
|
480
|
-
setTimeout(resolve, durationMs + 1000);
|
|
481
|
-
});
|
|
482
|
-
});
|
|
483
|
-
// ── arb alert (was arb gap alert) ──
|
|
484
|
-
arb
|
|
485
|
-
.command("alert")
|
|
486
|
-
.description("Wait until a symbol's price gap exceeds a threshold, then exit")
|
|
487
|
-
.requiredOption("-s, --symbol <sym>", "Symbol to watch")
|
|
488
|
-
.requiredOption("--above <pct>", "Gap % threshold to trigger")
|
|
489
|
-
.option("--interval <seconds>", "Check interval", "5")
|
|
490
|
-
.action(async (opts) => {
|
|
491
|
-
const symbol = opts.symbol.toUpperCase();
|
|
492
|
-
const threshold = parseFloat(opts.above);
|
|
493
|
-
const intervalMs = parseInt(opts.interval) * 1000;
|
|
494
|
-
if (!isJson())
|
|
495
|
-
console.log(chalk.cyan(`\n Waiting for ${symbol} gap > ${threshold}%...\n`));
|
|
496
|
-
const check = async () => {
|
|
497
|
-
const snapshots = await fetchAllPrices();
|
|
498
|
-
const s = snapshots.find((x) => x.symbol === symbol);
|
|
499
|
-
if (!s)
|
|
500
|
-
return false;
|
|
501
|
-
const now = new Date().toLocaleTimeString();
|
|
502
|
-
if (!isJson())
|
|
503
|
-
console.log(chalk.gray(` ${now} ${symbol} gap: ${s.maxGapPct.toFixed(4)}%`));
|
|
504
|
-
if (s.maxGapPct >= threshold) {
|
|
505
|
-
if (isJson()) {
|
|
506
|
-
printJson(jsonOk(s));
|
|
507
|
-
}
|
|
508
|
-
else {
|
|
509
|
-
console.log(chalk.green.bold(`\n TRIGGERED! ${symbol} gap: ${s.maxGapPct.toFixed(4)}% (> ${threshold}%)`));
|
|
510
|
-
const lines = [];
|
|
511
|
-
if (s.pacPrice !== null)
|
|
512
|
-
lines.push(` Pacifica: $${formatGapPrice(s.pacPrice)}`);
|
|
513
|
-
if (s.hlPrice !== null)
|
|
514
|
-
lines.push(` Hyperliquid: $${formatGapPrice(s.hlPrice)}`);
|
|
515
|
-
if (s.ltPrice !== null)
|
|
516
|
-
lines.push(` Lighter: $${formatGapPrice(s.ltPrice)}`);
|
|
517
|
-
lines.push(` Gap: $${s.maxGap.toFixed(4)} (${s.cheapest}→${s.expensive})`);
|
|
518
|
-
console.log(lines.join("\n") + "\n");
|
|
519
|
-
}
|
|
520
|
-
return true;
|
|
521
|
-
}
|
|
522
|
-
return false;
|
|
523
|
-
};
|
|
524
|
-
if (await check())
|
|
525
|
-
return;
|
|
526
|
-
await new Promise((resolve) => {
|
|
527
|
-
const timer = setInterval(async () => {
|
|
528
|
-
if (await check()) {
|
|
529
|
-
clearInterval(timer);
|
|
530
|
-
resolve();
|
|
531
|
-
}
|
|
532
|
-
}, intervalMs);
|
|
533
|
-
});
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
// registerGapSubcommands removed — use arb prices/watch/track/alert directly
|
|
537
176
|
// ────────────────────────────────────────────────────────
|
|
538
177
|
// Funding subcommands (merged from funding.ts)
|
|
539
178
|
// ────────────────────────────────────────────────────────
|
|
@@ -590,162 +229,245 @@ function printDetailedComparison(comparison) {
|
|
|
590
229
|
console.log(` Direction: Long ${exAbbr(comparison.longExchange)} / Short ${exAbbr(comparison.shortExchange)}`);
|
|
591
230
|
console.log(` Est. Income: $${comparison.estHourlyIncomeUsd.toFixed(4)}/hr per $1K notional\n`);
|
|
592
231
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
232
|
+
// registerFundingSubcommands removed — use 'arb scan --rates/--compare/--history/--positions' instead
|
|
233
|
+
// ── Exported handler functions for use by arb-auto.ts scan routing ──
|
|
234
|
+
export async function handleRates(isJson, opts) {
|
|
235
|
+
const minSpread = parseFloat(opts.minSpread);
|
|
236
|
+
let filterSymbols;
|
|
237
|
+
if (opts.symbol)
|
|
238
|
+
filterSymbols = [opts.symbol.toUpperCase()];
|
|
239
|
+
else if (opts.symbols)
|
|
240
|
+
filterSymbols = opts.symbols.split(",").map(s => s.trim().toUpperCase());
|
|
241
|
+
else if (!opts.all)
|
|
242
|
+
filterSymbols = TOP_SYMBOLS;
|
|
243
|
+
if (!isJson())
|
|
244
|
+
console.log(chalk.cyan(" Fetching funding rates from all exchanges...\n"));
|
|
245
|
+
const snapshot = await fetchAllFundingRates({ symbols: filterSymbols, minSpread });
|
|
246
|
+
try {
|
|
247
|
+
const allRates = snapshot.symbols.flatMap(s => s.rates);
|
|
248
|
+
if (allRates.length > 0)
|
|
249
|
+
saveFundingSnapshot(allRates);
|
|
250
|
+
}
|
|
251
|
+
catch { /* non-critical */ }
|
|
252
|
+
if (isJson()) {
|
|
253
|
+
if (process.argv.includes("--ndjson") || process.argv.includes("--fields"))
|
|
254
|
+
return printJson(jsonOk(snapshot.symbols));
|
|
255
|
+
return printJson(jsonOk(snapshot));
|
|
256
|
+
}
|
|
257
|
+
printSnapshotTable(snapshot);
|
|
258
|
+
printFundingExchangeStatus(snapshot);
|
|
259
|
+
}
|
|
260
|
+
export async function handleBasisScan(isJson, opts) {
|
|
261
|
+
if (!isJson())
|
|
262
|
+
console.log(chalk.cyan(" Fetching prices for basis calculation...\n"));
|
|
263
|
+
const [pacRates, hlRates, ltRates] = await Promise.all([fetchPacificaRates(), fetchHyperliquidRates(), fetchLighterRates()]);
|
|
264
|
+
const exchangePrices = new Map();
|
|
265
|
+
for (const r of [...pacRates, ...hlRates, ...ltRates]) {
|
|
266
|
+
if (r.markPrice <= 0)
|
|
267
|
+
continue;
|
|
268
|
+
if (!exchangePrices.has(r.symbol))
|
|
269
|
+
exchangePrices.set(r.symbol, new Map());
|
|
270
|
+
exchangePrices.get(r.symbol).set(r.exchange, r.markPrice);
|
|
271
|
+
}
|
|
272
|
+
const opps = [];
|
|
273
|
+
for (const [symbol, prices] of exchangePrices.entries()) {
|
|
274
|
+
if (prices.size < 2)
|
|
275
|
+
continue;
|
|
276
|
+
let minEx = "", maxEx = "", minPrice = Infinity, maxPrice = 0;
|
|
277
|
+
for (const [ex, price] of prices) {
|
|
278
|
+
if (price < minPrice) {
|
|
279
|
+
minPrice = price;
|
|
280
|
+
minEx = ex;
|
|
281
|
+
}
|
|
282
|
+
if (price > maxPrice) {
|
|
283
|
+
maxPrice = price;
|
|
284
|
+
maxEx = ex;
|
|
285
|
+
}
|
|
613
286
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
287
|
+
if (minEx === maxEx)
|
|
288
|
+
continue;
|
|
289
|
+
const diff = maxPrice - minPrice;
|
|
290
|
+
const avgPrice = (maxPrice + minPrice) / 2;
|
|
291
|
+
const pctDiff = (diff / avgPrice) * 100;
|
|
292
|
+
if (pctDiff * 365 >= parseFloat(opts.minBasis)) {
|
|
293
|
+
const priceObj = {};
|
|
294
|
+
for (const [ex, p] of prices)
|
|
295
|
+
priceObj[ex] = p;
|
|
296
|
+
opps.push({ symbol, prices: priceObj, priceDiff: diff, pctDiff, buyOn: minEx, sellOn: maxEx });
|
|
619
297
|
}
|
|
620
|
-
|
|
621
|
-
|
|
298
|
+
}
|
|
299
|
+
opps.sort((a, b) => b.pctDiff - a.pctDiff);
|
|
300
|
+
if (isJson())
|
|
301
|
+
return printJson(jsonOk(opps));
|
|
302
|
+
if (opps.length === 0) {
|
|
303
|
+
console.log(chalk.gray(` No basis opportunities above ${opts.minBasis}% threshold.\n`));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
console.log(chalk.cyan.bold(" Cross-Exchange Basis Opportunities\n"));
|
|
307
|
+
console.log(chalk.gray(" Strategy: Buy cheap, sell expensive, profit from convergence.\n"));
|
|
308
|
+
const rows = opps.map((o) => [
|
|
309
|
+
chalk.white.bold(o.symbol),
|
|
310
|
+
o.prices.pacifica ? `$${formatUsd(o.prices.pacifica)}` : chalk.gray("-"),
|
|
311
|
+
o.prices.hyperliquid ? `$${formatUsd(o.prices.hyperliquid)}` : chalk.gray("-"),
|
|
312
|
+
o.prices.lighter ? `$${formatUsd(o.prices.lighter)}` : chalk.gray("-"),
|
|
313
|
+
`$${formatUsd(o.priceDiff)}`, `${o.pctDiff.toFixed(4)}%`, chalk.green(o.buyOn), chalk.red(o.sellOn),
|
|
314
|
+
]);
|
|
315
|
+
console.log(makeTable(["Symbol", "Pacifica", "Hyperliquid", "Lighter", "Diff", "Basis %", "Buy On", "Sell On"], rows));
|
|
316
|
+
}
|
|
317
|
+
export async function handleDexScan(isJson, opts) {
|
|
318
|
+
if (!isJson())
|
|
319
|
+
console.log(chalk.cyan(" Scanning HIP-3 deployed dex funding spreads...\n"));
|
|
320
|
+
const pairs = await scanDexArb({ minAnnualSpread: parseFloat(opts.minSpread), maxPriceGapPct: parseFloat(opts.maxGap), includeNative: opts.includeNative });
|
|
321
|
+
const topN = parseInt(opts.top);
|
|
322
|
+
const shown = pairs.slice(0, topN);
|
|
323
|
+
if (isJson())
|
|
324
|
+
return printJson(jsonOk(shown.map(p => ({
|
|
325
|
+
underlying: p.underlying, longDex: p.long.dex, longSymbol: p.long.raw, longFunding: p.long.fundingRate,
|
|
326
|
+
longPrice: p.long.markPrice, longOiUsd: p.long.openInterest * p.long.markPrice, longVolume24h: p.long.volume24h,
|
|
327
|
+
shortDex: p.short.dex, shortSymbol: p.short.raw, shortFunding: p.short.fundingRate, shortPrice: p.short.markPrice,
|
|
328
|
+
shortOiUsd: p.short.openInterest * p.short.markPrice, shortVolume24h: p.short.volume24h,
|
|
329
|
+
annualSpread: p.annualSpread, priceGapPct: p.priceGapPct, minOiUsd: p.minOiUsd, minVolume24hUsd: p.minVolume24hUsd, viability: p.viability,
|
|
330
|
+
}))));
|
|
331
|
+
if (shown.length === 0) {
|
|
332
|
+
console.log(chalk.gray(` No cross-dex opportunities above ${opts.minSpread}% annual spread.\n`));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
console.log(chalk.cyan.bold(" HIP-3 Cross-Dex Funding Arb Opportunities\n"));
|
|
336
|
+
console.log(chalk.gray(" Strategy: Long on low-funding dex, Short on high-funding dex (same underlying)\n"));
|
|
337
|
+
const rows = shown.map((p) => {
|
|
338
|
+
const spreadColor = p.annualSpread >= 50 ? chalk.green.bold : p.annualSpread >= 20 ? chalk.green : chalk.yellow;
|
|
339
|
+
const viabilityColor = p.viability === "A" ? chalk.green.bold : p.viability === "B" ? chalk.green : p.viability === "C" ? chalk.yellow : chalk.red;
|
|
340
|
+
const fmtOi = p.minOiUsd >= 1_000_000 ? `$${(p.minOiUsd / 1_000_000).toFixed(1)}M` : p.minOiUsd >= 1_000 ? `$${(p.minOiUsd / 1_000).toFixed(0)}K` : `$${p.minOiUsd.toFixed(0)}`;
|
|
341
|
+
const fmtVol = p.minVolume24hUsd >= 1_000_000 ? `$${(p.minVolume24hUsd / 1_000_000).toFixed(1)}M` : p.minVolume24hUsd >= 1_000 ? `$${(p.minVolume24hUsd / 1_000).toFixed(0)}K` : `$${p.minVolume24hUsd.toFixed(0)}`;
|
|
342
|
+
return [chalk.white.bold(p.underlying), `${p.long.dex}:${p.long.base}`, formatPercent(p.long.fundingRate), `${p.short.dex}:${p.short.base}`, formatPercent(p.short.fundingRate), `$${formatUsd(p.long.markPrice)}`, spreadColor(`${p.annualSpread.toFixed(1)}%`), p.priceGapPct < 0.1 ? chalk.green(`${p.priceGapPct.toFixed(3)}%`) : chalk.yellow(`${p.priceGapPct.toFixed(3)}%`), viabilityColor(p.viability), viabilityColor(fmtOi), chalk.gray(fmtVol)];
|
|
622
343
|
});
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
344
|
+
console.log(makeTable(["Asset", "Long On", "L.Fund", "Short On", "S.Fund", "Price", "Ann.Spread", "Gap", "Grade", "MinOI", "MinVol"], rows));
|
|
345
|
+
const dexes = new Set();
|
|
346
|
+
for (const p of shown) {
|
|
347
|
+
dexes.add(p.long.dex);
|
|
348
|
+
dexes.add(p.short.dex);
|
|
349
|
+
}
|
|
350
|
+
const gradeCount = { A: 0, B: 0, C: 0, D: 0 };
|
|
351
|
+
for (const p of shown)
|
|
352
|
+
gradeCount[p.viability]++;
|
|
353
|
+
console.log(chalk.gray(`\n ${shown.length}/${pairs.length} opportunities | Dexes: ${[...dexes].sort().join(", ")}`));
|
|
354
|
+
console.log(chalk.gray(` Grade: ${chalk.green.bold(`A(${gradeCount.A})`)} ${chalk.green(`B(${gradeCount.B})`)} ${chalk.yellow(`C(${gradeCount.C})`)} ${chalk.red(`D(${gradeCount.D})`)}`));
|
|
355
|
+
console.log(chalk.gray(` A: OI>$1M B: OI>$100K C: OI>$10K D: OI<$10K (thin — high risk)`));
|
|
356
|
+
console.log(chalk.gray(` Note: HIP-3 dexes charge 2x fees. Factor into net profitability.\n`));
|
|
357
|
+
}
|
|
358
|
+
export async function handleCompare(isJson, symbol) {
|
|
359
|
+
if (!isJson())
|
|
360
|
+
console.log(chalk.cyan(` Fetching funding rates for ${symbol.toUpperCase()}...\n`));
|
|
361
|
+
const comparison = await fetchSymbolFundingRates(symbol);
|
|
362
|
+
if (!comparison) {
|
|
634
363
|
if (isJson())
|
|
635
|
-
return printJson(jsonOk(
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
data[ex] = rates;
|
|
652
|
-
}
|
|
653
|
-
return printJson(jsonOk({ symbol, hours, startTime: startTime.toISOString(), endTime: endTime.toISOString(), rates: data }));
|
|
654
|
-
}
|
|
655
|
-
console.log(chalk.cyan.bold(` ${symbol} Funding Rate History (last ${hours}h)\n`));
|
|
656
|
-
const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
|
|
657
|
-
let hasData = false;
|
|
364
|
+
return printJson(jsonOk({ symbol: symbol.toUpperCase(), available: false }));
|
|
365
|
+
console.log(chalk.gray(` ${symbol.toUpperCase()} not found on at least 2 exchanges.\n`));
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (isJson())
|
|
369
|
+
return printJson(jsonOk(comparison));
|
|
370
|
+
printDetailedComparison(comparison);
|
|
371
|
+
}
|
|
372
|
+
export async function handleFundingHistory(isJson, opts) {
|
|
373
|
+
const symbol = opts.symbol.toUpperCase();
|
|
374
|
+
const hours = parseInt(opts.hours);
|
|
375
|
+
const exchanges = opts.exchange ? [opts.exchange.toLowerCase()] : ["hyperliquid", "pacifica", "lighter"];
|
|
376
|
+
const endTime = new Date();
|
|
377
|
+
const startTime = new Date(endTime.getTime() - hours * 3600000);
|
|
378
|
+
if (isJson()) {
|
|
379
|
+
const data = {};
|
|
658
380
|
for (const ex of exchanges) {
|
|
659
381
|
const rates = getHistoricalRates(symbol, ex, startTime, endTime);
|
|
660
|
-
if (rates.length
|
|
661
|
-
|
|
662
|
-
hasData = true;
|
|
663
|
-
console.log(chalk.white.bold(` ${exAbbr(ex)} (${rates.length} snapshots):`));
|
|
664
|
-
const rows = rates.map(r => { const color = r.rate > 0 ? chalk.red : r.rate < 0 ? chalk.green : chalk.white; return [chalk.gray(new Date(r.ts).toLocaleString()), color(formatPercent(r.rate)), `${(r.hourlyRate * 100).toFixed(6)}%/h`, `${annualizeHourlyRate(r.hourlyRate).toFixed(2)}%/yr`]; });
|
|
665
|
-
console.log(makeTable(["Time", "Raw Rate", "Hourly", "Annualized"], rows));
|
|
666
|
-
console.log();
|
|
382
|
+
if (rates.length > 0)
|
|
383
|
+
data[ex] = rates;
|
|
667
384
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
if (
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
const sz = Math.abs(Number(pos.size));
|
|
719
|
-
const nu = sz * mk;
|
|
720
|
-
const hr = toHourlyRate(fr, exchange);
|
|
721
|
-
const ap = annualizeRate(fr, exchange);
|
|
722
|
-
const hp = estimateHourlyFunding(fr, exchange, nu, pos.side);
|
|
723
|
-
const a = af.get(sym);
|
|
724
|
-
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) });
|
|
725
|
-
}
|
|
385
|
+
return printJson(jsonOk({ symbol, hours, startTime: startTime.toISOString(), endTime: endTime.toISOString(), rates: data }));
|
|
386
|
+
}
|
|
387
|
+
console.log(chalk.cyan.bold(` ${symbol} Funding Rate History (last ${hours}h)\n`));
|
|
388
|
+
const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
|
|
389
|
+
let hasData = false;
|
|
390
|
+
for (const ex of exchanges) {
|
|
391
|
+
const rates = getHistoricalRates(symbol, ex, startTime, endTime);
|
|
392
|
+
if (rates.length === 0)
|
|
393
|
+
continue;
|
|
394
|
+
hasData = true;
|
|
395
|
+
console.log(chalk.white.bold(` ${exAbbr(ex)} (${rates.length} snapshots):`));
|
|
396
|
+
const rows = rates.map(r => { const color = r.rate > 0 ? chalk.red : r.rate < 0 ? chalk.green : chalk.white; return [chalk.gray(new Date(r.ts).toLocaleString()), color(formatPercent(r.rate)), `${(r.hourlyRate * 100).toFixed(6)}%/h`, `${annualizeHourlyRate(r.hourlyRate).toFixed(2)}%/yr`]; });
|
|
397
|
+
console.log(makeTable(["Time", "Raw Rate", "Hourly", "Annualized"], rows));
|
|
398
|
+
console.log();
|
|
399
|
+
}
|
|
400
|
+
if (!hasData)
|
|
401
|
+
console.log(chalk.gray(` No historical data. Run 'perp arb rates' to start collecting.\n`));
|
|
402
|
+
}
|
|
403
|
+
export async function handleFundingPositions(isJson, getAdapterForExchange, opts) {
|
|
404
|
+
await withJsonErrors(isJson(), async () => {
|
|
405
|
+
const targetExchanges = opts.exchanges ? opts.exchanges.split(",").map(s => s.trim().toLowerCase()) : [...ALL_EXCHANGES];
|
|
406
|
+
if (!isJson())
|
|
407
|
+
console.log(chalk.cyan(" Fetching positions and funding rates...\n"));
|
|
408
|
+
const results = [];
|
|
409
|
+
const exchangeErrors = {};
|
|
410
|
+
await Promise.all(targetExchanges.map(async (exchange) => {
|
|
411
|
+
try {
|
|
412
|
+
const adapter = await getAdapterForExchange(exchange);
|
|
413
|
+
const [positions, markets, fp] = await Promise.all([adapter.getPositions(), adapter.getMarkets(), adapter.getFundingPayments(100).catch(() => [])]);
|
|
414
|
+
if (positions.length === 0)
|
|
415
|
+
return;
|
|
416
|
+
const fm = new Map();
|
|
417
|
+
for (const m of markets) {
|
|
418
|
+
const sym = m.symbol.toUpperCase().replace(/-PERP$/, "");
|
|
419
|
+
fm.set(sym, { rate: Number(m.fundingRate) || 0, markPrice: Number(m.markPrice) || 0 });
|
|
420
|
+
}
|
|
421
|
+
const dayAgo = Date.now() - 86400000;
|
|
422
|
+
const af = new Map();
|
|
423
|
+
for (const f of fp) {
|
|
424
|
+
if (f.time < dayAgo)
|
|
425
|
+
continue;
|
|
426
|
+
const sym = f.symbol.toUpperCase().replace(/-PERP$/, "");
|
|
427
|
+
const amt = Number(f.payment) || 0;
|
|
428
|
+
if (!af.has(sym))
|
|
429
|
+
af.set(sym, { received: 0, paid: 0 });
|
|
430
|
+
const e = af.get(sym);
|
|
431
|
+
if (amt > 0)
|
|
432
|
+
e.received += amt;
|
|
433
|
+
else
|
|
434
|
+
e.paid += Math.abs(amt);
|
|
726
435
|
}
|
|
727
|
-
|
|
728
|
-
|
|
436
|
+
for (const pos of positions) {
|
|
437
|
+
const sym = pos.symbol.toUpperCase().replace(/-PERP$/, "");
|
|
438
|
+
const fd = fm.get(sym);
|
|
439
|
+
const fr = fd?.rate ?? 0;
|
|
440
|
+
const mk = Number(pos.markPrice) || fd?.markPrice || 0;
|
|
441
|
+
const sz = Math.abs(Number(pos.size));
|
|
442
|
+
const nu = sz * mk;
|
|
443
|
+
const hr = toHourlyRate(fr, exchange);
|
|
444
|
+
const ap = annualizeRate(fr, exchange);
|
|
445
|
+
const hp = estimateHourlyFunding(fr, exchange, nu, pos.side);
|
|
446
|
+
const a = af.get(sym);
|
|
447
|
+
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) });
|
|
729
448
|
}
|
|
730
|
-
}));
|
|
731
|
-
if (results.length === 0) {
|
|
732
|
-
if (isJson())
|
|
733
|
-
return printJson(jsonOk({ positions: [], totals: { predicted: { hourly: 0, daily: 0 }, actual24h: { net: 0 }, notionalUsd: 0 }, errors: exchangeErrors }));
|
|
734
|
-
console.log(chalk.gray(" No open positions.\n"));
|
|
735
|
-
return;
|
|
736
449
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
450
|
+
catch (err) {
|
|
451
|
+
exchangeErrors[exchange] = err instanceof Error ? err.message : String(err);
|
|
452
|
+
}
|
|
453
|
+
}));
|
|
454
|
+
if (results.length === 0) {
|
|
742
455
|
if (isJson())
|
|
743
|
-
return printJson(jsonOk({ positions:
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
456
|
+
return printJson(jsonOk({ positions: [], totals: { predicted: { hourly: 0, daily: 0 }, actual24h: { net: 0 }, notionalUsd: 0 }, errors: exchangeErrors }));
|
|
457
|
+
console.log(chalk.gray(" No open positions.\n"));
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
results.sort((a, b) => Math.abs(b.dailyPayment) - Math.abs(a.dailyPayment));
|
|
461
|
+
const tH = results.reduce((s, r) => s + r.hourlyPayment, 0);
|
|
462
|
+
const tD = results.reduce((s, r) => s + r.dailyPayment, 0);
|
|
463
|
+
const tN = results.reduce((s, r) => s + r.notionalUsd, 0);
|
|
464
|
+
const tA = results.reduce((s, r) => s + r.actualNet24h, 0);
|
|
465
|
+
if (isJson())
|
|
466
|
+
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) }, errors: Object.keys(exchangeErrors).length > 0 ? exchangeErrors : undefined }));
|
|
467
|
+
const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : e.toUpperCase();
|
|
468
|
+
console.log(chalk.cyan.bold(" Position Funding Impact\n"));
|
|
469
|
+
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)]; });
|
|
470
|
+
console.log(makeTable(["Ex", "Symbol", "Side", "Notional", "Rate(now)", "Annual(now)", "Actual 24h", "uPnL"], rows));
|
|
471
|
+
console.log(`\n Total: $${formatUsd(tN)} | Actual 24h: ${tA >= 0 ? "+" : ""}$${tA.toFixed(4)} | Predicted: ${tD >= 0 ? "-" : "+"}$${Math.abs(tD).toFixed(4)}/d\n`);
|
|
750
472
|
});
|
|
751
473
|
}
|