perp-cli 0.5.2 → 0.6.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/README.md +29 -0
- package/dist/commands/account.js +60 -0
- package/dist/commands/agent.js +4 -2
- package/dist/commands/alerts.d.ts +2 -0
- package/dist/commands/alerts.js +487 -0
- package/dist/commands/arb/index.js +12 -4
- package/dist/commands/arb-auto.js +50 -19
- package/dist/commands/arb.js +43 -4
- package/dist/commands/history.js +329 -70
- package/dist/discord.d.ts +29 -0
- package/dist/discord.js +66 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +1 -0
- package/dist/exchanges/hyperliquid-spot.js +25 -10
- package/dist/exchanges/hyperliquid.d.ts +2 -0
- package/dist/exchanges/hyperliquid.js +10 -2
- package/dist/exchanges/lighter-spot.d.ts +6 -0
- package/dist/exchanges/lighter-spot.js +80 -31
- package/dist/exchanges/lighter.d.ts +3 -0
- package/dist/exchanges/lighter.js +46 -25
- package/dist/exchanges/pacifica.js +26 -4
- package/dist/index.js +325 -43
- package/dist/mcp-server.js +444 -1
- package/dist/settings.d.ts +26 -0
- package/dist/settings.js +14 -2
- package/dist/telegram.d.ts +32 -0
- package/dist/telegram.js +74 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +32 -0
- package/package.json +1 -1
- package/skills/perp-cli/package.json +35 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { formatUsd, printJson, jsonOk, makeTable } from "../utils.js";
|
|
3
3
|
import { computeExecutableSize } from "../liquidity.js";
|
|
4
|
+
import { computeMatchedSize, computeSpotPerpMatchedSize } from "../arb/sizing.js";
|
|
4
5
|
import { computeAnnualSpread, toHourlyRate } from "../funding.js";
|
|
5
6
|
import { fetchPacificaPricesRaw, parsePacificaRaw, fetchHyperliquidMetaRaw, parseHyperliquidMetaRaw, fetchLighterOrderBookDetailsRaw, fetchLighterFundingRatesRaw, parseLighterRaw, } from "../shared-api.js";
|
|
6
7
|
import { scanDexArb } from "../dex-asset-map.js";
|
|
@@ -1390,7 +1391,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1390
1391
|
return;
|
|
1391
1392
|
}
|
|
1392
1393
|
// 4. Compute matched size in base asset (use the smaller side)
|
|
1393
|
-
//
|
|
1394
|
+
// Uses computeMatchedSize which enforces exchange min order values
|
|
1394
1395
|
const inferDecimals = (levels) => {
|
|
1395
1396
|
for (const [, s] of levels) {
|
|
1396
1397
|
const dot = s.indexOf(".");
|
|
@@ -1401,11 +1402,19 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1401
1402
|
};
|
|
1402
1403
|
const longDecimals = inferDecimals(longBook.asks);
|
|
1403
1404
|
const shortDecimals = inferDecimals(shortBook.bids);
|
|
1404
|
-
const decimals = Math.min(longDecimals, shortDecimals);
|
|
1405
1405
|
const matchedBase = Math.min(longCheck.recommendedSize, shortCheck.recommendedSize);
|
|
1406
|
-
const matchedSize = (Math.floor(matchedBase * 10 ** decimals) / 10 ** decimals).toFixed(decimals);
|
|
1407
1406
|
const avgPrice = (longCheck.avgFillPrice + shortCheck.avgFillPrice) / 2;
|
|
1408
|
-
const
|
|
1407
|
+
const matchedUsdRaw = matchedBase * avgPrice;
|
|
1408
|
+
const sizing = computeMatchedSize(matchedUsdRaw, avgPrice, longExch, shortExch, { longSizeDecimals: longDecimals, shortSizeDecimals: shortDecimals });
|
|
1409
|
+
if (!sizing) {
|
|
1410
|
+
const msg = `Order too small: $${matchedUsdRaw.toFixed(2)} below exchange minimum. Request a larger size.`;
|
|
1411
|
+
if (isJson())
|
|
1412
|
+
return printJson(jsonOk({ error: msg, computedUsd: matchedUsdRaw }));
|
|
1413
|
+
console.log(chalk.red(`\n ${msg}`));
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
const matchedSize = sizing.size;
|
|
1417
|
+
const matchedUsd = sizing.notional;
|
|
1409
1418
|
if (!isJson()) {
|
|
1410
1419
|
console.log(chalk.cyan(`\n Arb Exec: ${sym}${opts.smart ? " (smart order)" : ""}${dryRun ? " [DRY RUN]" : ""}`));
|
|
1411
1420
|
console.log(chalk.gray(` LONG ${longExch}: buy ${matchedSize} (~$${(matchedBase * longCheck.avgFillPrice).toFixed(2)}, slippage ${longCheck.slippagePct.toFixed(3)}%)`));
|
|
@@ -1689,22 +1698,35 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1689
1698
|
// Continue — spot order may still work if spot balance already exists
|
|
1690
1699
|
}
|
|
1691
1700
|
}
|
|
1692
|
-
// 2b. Verify
|
|
1701
|
+
// 2b. Verify balance is sufficient after transfer
|
|
1693
1702
|
try {
|
|
1694
|
-
|
|
1695
|
-
const
|
|
1696
|
-
|
|
1697
|
-
|
|
1703
|
+
// HL unified account: perp equity IS spot balance (shared)
|
|
1704
|
+
const isUnified = spotExch === "hyperliquid";
|
|
1705
|
+
let spotUsdcAvail;
|
|
1706
|
+
if (isUnified) {
|
|
1707
|
+
// Unified account: perp adapter's available balance = spot purchasing power
|
|
1708
|
+
const hlBalance = await perpAdapter.getBalance();
|
|
1709
|
+
spotUsdcAvail = Number(hlBalance?.available ?? 0);
|
|
1710
|
+
}
|
|
1711
|
+
else {
|
|
1712
|
+
const spotBalances = await spotAdapter.getSpotBalances();
|
|
1713
|
+
const spotUsdc = spotBalances.find(b => b.token === "USDC_SPOT" || b.token === "USDC");
|
|
1714
|
+
spotUsdcAvail = Number(spotUsdc?.available ?? 0);
|
|
1715
|
+
}
|
|
1716
|
+
// Check perp balance (for perp leg margin)
|
|
1698
1717
|
const perpBalance = await perpAdapter.getBalance();
|
|
1699
1718
|
const perpAvail = Number(perpBalance?.available ?? 0);
|
|
1700
|
-
|
|
1701
|
-
|
|
1719
|
+
// For unified accounts (same exchange), total needed = spotSize + perpMargin from same pool
|
|
1720
|
+
const totalNeeded = spotExch === perpExch ? sizeUsd * 1.5 : sizeUsd;
|
|
1721
|
+
const checkBalance = spotExch === perpExch ? spotUsdcAvail : spotUsdcAvail;
|
|
1722
|
+
if (checkBalance < totalNeeded * 0.9) {
|
|
1723
|
+
const msg = `Insufficient balance: have $${spotUsdcAvail.toFixed(2)}, need ~$${totalNeeded.toFixed(0)} (spot + perp margin)`;
|
|
1702
1724
|
if (isJson())
|
|
1703
|
-
return printJson(jsonOk({ error: msg, spotUsdcAvail, required:
|
|
1725
|
+
return printJson(jsonOk({ error: msg, spotUsdcAvail, required: totalNeeded }));
|
|
1704
1726
|
console.log(chalk.red(` ${msg}`));
|
|
1705
1727
|
return;
|
|
1706
1728
|
}
|
|
1707
|
-
if (perpAvail < sizeUsd * 0.5) {
|
|
1729
|
+
if (spotExch !== perpExch && perpAvail < sizeUsd * 0.5) {
|
|
1708
1730
|
const msg = `Insufficient perp balance: have $${perpAvail.toFixed(2)} available on ${perpExch}, need margin for ~$${sizeUsd}`;
|
|
1709
1731
|
if (isJson())
|
|
1710
1732
|
return printJson(jsonOk({ error: msg, perpAvail, required: sizeUsd }));
|
|
@@ -1763,7 +1785,8 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1763
1785
|
console.log(chalk.gray(` ${perpExch} perp bids: $${perpCheck.depthUsd.toFixed(0)}`));
|
|
1764
1786
|
return;
|
|
1765
1787
|
}
|
|
1766
|
-
// 4. Compute matched size
|
|
1788
|
+
// 4. Compute matched size (with min order value enforcement)
|
|
1789
|
+
const avgPrice = (spotCheck.avgFillPrice + perpCheck.avgFillPrice) / 2;
|
|
1767
1790
|
const inferDecimals = (levels) => {
|
|
1768
1791
|
for (const [, s] of levels) {
|
|
1769
1792
|
const dot = s.indexOf(".");
|
|
@@ -1773,12 +1796,20 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1773
1796
|
return 0;
|
|
1774
1797
|
};
|
|
1775
1798
|
const spotDecimals = inferDecimals(spotBook.asks);
|
|
1776
|
-
const perpDecimals = inferDecimals(perpBook.bids);
|
|
1777
|
-
const decimals = Math.min(spotDecimals, perpDecimals);
|
|
1778
1799
|
const matchedBase = Math.min(spotCheck.recommendedSize, perpCheck.recommendedSize);
|
|
1779
|
-
const
|
|
1780
|
-
|
|
1781
|
-
|
|
1800
|
+
const matchedUsdRaw = matchedBase * avgPrice;
|
|
1801
|
+
// Use computeSpotPerpMatchedSize which enforces exchange min order values
|
|
1802
|
+
// (e.g. HL $10 min) — rounds UP if floor would drop below minimum
|
|
1803
|
+
const sizing = computeSpotPerpMatchedSize(matchedUsdRaw, avgPrice, spotExch, perpExch, spotDecimals);
|
|
1804
|
+
if (!sizing) {
|
|
1805
|
+
const msg = `Order too small: $${matchedUsdRaw.toFixed(2)} below exchange minimum. Request a larger size.`;
|
|
1806
|
+
if (isJson())
|
|
1807
|
+
return printJson(jsonOk({ error: msg, computedUsd: matchedUsdRaw }));
|
|
1808
|
+
console.log(chalk.red(`\n ${msg}`));
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
const matchedSize = sizing.size;
|
|
1812
|
+
const matchedUsd = sizing.notional;
|
|
1782
1813
|
if (!isJson()) {
|
|
1783
1814
|
console.log(chalk.cyan(`\n Spot+Perp Arb: ${sym}`));
|
|
1784
1815
|
console.log(chalk.gray(` SPOT ${spotExch}: buy ${matchedSize} (~$${(matchedBase * spotCheck.avgFillPrice).toFixed(2)})`));
|
package/dist/commands/arb.js
CHANGED
|
@@ -190,21 +190,50 @@ function printSnapshotTable(snapshot) {
|
|
|
190
190
|
console.log(chalk.gray(" No funding rate data available.\n"));
|
|
191
191
|
return;
|
|
192
192
|
}
|
|
193
|
+
// Build 24h trend: arrow + change in annualized spread
|
|
194
|
+
const now = new Date();
|
|
195
|
+
const h24ago = new Date(now.getTime() - 24 * 3600_000);
|
|
196
|
+
const trendMap = new Map(); // symbol → "▲+12.3%" or "▼-5.1%"
|
|
197
|
+
for (const s of snapshot.symbols) {
|
|
198
|
+
const bestEx = s.rates.find(r => r.exchange === "hyperliquid")?.exchange
|
|
199
|
+
?? s.rates.find(r => r.exchange === "pacifica")?.exchange
|
|
200
|
+
?? s.rates[0]?.exchange ?? "hyperliquid";
|
|
201
|
+
const history = getHistoricalRates(s.symbol, bestEx, h24ago, now);
|
|
202
|
+
if (history.length >= 2) {
|
|
203
|
+
const oldRate = history[0].hourlyRate;
|
|
204
|
+
const newRate = history[history.length - 1].hourlyRate;
|
|
205
|
+
// Change in annualized rate (hourly × 8760)
|
|
206
|
+
const oldAnn = Math.abs(oldRate) * 8760 * 100;
|
|
207
|
+
const newAnn = Math.abs(newRate) * 8760 * 100;
|
|
208
|
+
const delta = newAnn - oldAnn;
|
|
209
|
+
const absDelta = Math.abs(delta);
|
|
210
|
+
if (absDelta < 1) {
|
|
211
|
+
trendMap.set(s.symbol, chalk.gray(`\u2500 ${absDelta.toFixed(1)}%`)); // ─ stable
|
|
212
|
+
}
|
|
213
|
+
else if (delta > 0) {
|
|
214
|
+
trendMap.set(s.symbol, chalk.red(`\u25B2+${absDelta.toFixed(1)}%`)); // ▲ rising
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
trendMap.set(s.symbol, chalk.green(`\u25BC-${absDelta.toFixed(1)}%`)); // ▼ falling
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
193
221
|
const rows = snapshot.symbols.map(s => {
|
|
194
222
|
const pacRate = s.rates.find(r => r.exchange === "pacifica");
|
|
195
223
|
const hlRate = s.rates.find(r => r.exchange === "hyperliquid");
|
|
196
224
|
const ltRate = s.rates.find(r => r.exchange === "lighter");
|
|
197
225
|
const spreadColor = s.maxSpreadAnnual >= 30 ? chalk.green : s.maxSpreadAnnual >= 10 ? chalk.yellow : chalk.white;
|
|
198
226
|
const bestRate = hlRate ?? pacRate ?? ltRate;
|
|
227
|
+
const trend = trendMap.get(s.symbol) ?? chalk.gray("-");
|
|
199
228
|
return [
|
|
200
229
|
chalk.white.bold(s.symbol), pacRate ? formatPercent(pacRate.fundingRate) : chalk.gray("-"),
|
|
201
230
|
hlRate ? formatPercent(hlRate.fundingRate) : chalk.gray("-"), ltRate ? formatPercent(ltRate.fundingRate) : chalk.gray("-"),
|
|
202
|
-
spreadColor(`${s.maxSpreadAnnual.toFixed(1)}%`),
|
|
231
|
+
spreadColor(`${s.maxSpreadAnnual.toFixed(1)}%`), trend,
|
|
203
232
|
s.maxSpreadAnnual >= 5 ? `${exAbbr(s.shortExchange)}>${exAbbr(s.longExchange)}` : chalk.gray("-"),
|
|
204
233
|
formatAvgRate(bestRate?.historicalAvg?.avg8h), formatAvgRate(bestRate?.historicalAvg?.avg24h), formatAvgRate(bestRate?.historicalAvg?.avg7d),
|
|
205
234
|
];
|
|
206
235
|
});
|
|
207
|
-
console.log(makeTable(["Symbol", "Pacifica", "Hyperliquid", "Lighter", "
|
|
236
|
+
console.log(makeTable(["Symbol", "Pacifica", "Hyperliquid", "Lighter", "Spread", "24h", "Direction", "Avg 8h", "Avg 24h", "Avg 7d"], rows));
|
|
208
237
|
console.log(chalk.gray(`\n ${snapshot.symbols.length} symbols compared across exchanges.\n`));
|
|
209
238
|
}
|
|
210
239
|
function printFundingExchangeStatus(snapshot) {
|
|
@@ -250,9 +279,19 @@ export async function handleRates(isJson, opts) {
|
|
|
250
279
|
}
|
|
251
280
|
catch { /* non-critical */ }
|
|
252
281
|
if (isJson()) {
|
|
282
|
+
// Enrich with 24h rate history for each symbol
|
|
283
|
+
const now = new Date();
|
|
284
|
+
const h24ago = new Date(now.getTime() - 24 * 3600_000);
|
|
285
|
+
const enriched = snapshot.symbols.map(s => {
|
|
286
|
+
const bestEx = s.rates.find(r => r.exchange === "hyperliquid")?.exchange
|
|
287
|
+
?? s.rates.find(r => r.exchange === "pacifica")?.exchange
|
|
288
|
+
?? s.rates[0]?.exchange ?? "hyperliquid";
|
|
289
|
+
const history = getHistoricalRates(s.symbol, bestEx, h24ago, now);
|
|
290
|
+
return { ...s, rateHistory: history.map(h => ({ ts: h.ts, hourlyRate: h.hourlyRate })) };
|
|
291
|
+
});
|
|
253
292
|
if (process.argv.includes("--ndjson") || process.argv.includes("--fields"))
|
|
254
|
-
return printJson(jsonOk(
|
|
255
|
-
return printJson(jsonOk(snapshot));
|
|
293
|
+
return printJson(jsonOk(enriched));
|
|
294
|
+
return printJson(jsonOk({ ...snapshot, symbols: enriched }));
|
|
256
295
|
}
|
|
257
296
|
printSnapshotTable(snapshot);
|
|
258
297
|
printFundingExchangeStatus(snapshot);
|