perp-cli 0.15.0 → 0.16.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.ja.md +243 -0
- package/README.ko.md +243 -0
- package/README.md +129 -535
- package/README.zh.md +243 -0
- package/dist/api/public/lighter.d.ts +5 -0
- package/dist/api/public/lighter.js +5 -0
- package/dist/api/public/pacifica.js +2 -2
- package/dist/arb/sizing.js +4 -2
- package/dist/arb/state.d.ts +6 -0
- package/dist/balance-by-address.d.ts +39 -0
- package/dist/balance-by-address.js +44 -0
- package/dist/bot/quote-rules.js +1 -1
- package/dist/bot/strategies/funding-arb-strategy.js +10 -3
- package/dist/bot/strategies/funding-arb-utils.d.ts +14 -2
- package/dist/bot/strategies/funding-arb-utils.js +48 -15
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-arb-strategy.js +4 -2
- package/dist/bot/strategies/spot-perp-executor.js +2 -2
- package/dist/cli-program.js +1 -1
- package/dist/commands/account.js +168 -24
- package/dist/commands/arb/index.js +19 -77
- package/dist/commands/arb-auto.d.ts +24 -0
- package/dist/commands/arb-auto.js +155 -81
- package/dist/commands/arb.d.ts +8 -0
- package/dist/commands/arb.js +49 -23
- package/dist/commands/backtest.js +23 -21
- package/dist/commands/market.d.ts +48 -0
- package/dist/commands/market.js +241 -78
- package/dist/commands/options.d.ts +5 -0
- package/dist/commands/options.js +20 -3
- package/dist/commands/pacifica-earn.js +8 -21
- package/dist/commands/portfolio.d.ts +16 -0
- package/dist/commands/portfolio.js +51 -8
- package/dist/commands/trade.js +4 -4
- package/dist/cross-chain-margin.d.ts +3 -1
- package/dist/cross-chain-margin.js +4 -2
- package/dist/dashboard/server.js +2 -2
- package/dist/dashboard/ui.js +9 -6
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +10 -0
- package/dist/exchanges/address.d.ts +8 -0
- package/dist/exchanges/address.js +47 -0
- package/dist/exchanges/arcus/adapter.d.ts +10 -6
- package/dist/exchanges/arcus/adapter.js +33 -6
- package/dist/exchanges/arcus/public.js +3 -2
- package/dist/exchanges/aster.d.ts +27 -7
- package/dist/exchanges/aster.js +124 -42
- package/dist/exchanges/canonical-symbols.d.ts +7 -0
- package/dist/exchanges/canonical-symbols.js +20 -0
- package/dist/exchanges/capabilities.d.ts +2 -0
- package/dist/exchanges/capabilities.js +2 -1
- package/dist/exchanges/derive/options.d.ts +5 -0
- package/dist/exchanges/derive/options.js +20 -1
- package/dist/exchanges/extended/adapter.d.ts +16 -2
- package/dist/exchanges/extended/adapter.js +96 -34
- package/dist/exchanges/extended/public.d.ts +3 -0
- package/dist/exchanges/extended/public.js +9 -2
- package/dist/exchanges/grvt/adapter.d.ts +16 -2
- package/dist/exchanges/grvt/adapter.js +98 -21
- package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
- package/dist/exchanges/hyperliquid-outcome.js +29 -1
- package/dist/exchanges/hyperliquid-spot.d.ts +8 -0
- package/dist/exchanges/hyperliquid-spot.js +20 -12
- package/dist/exchanges/hyperliquid.d.ts +180 -13
- package/dist/exchanges/hyperliquid.js +487 -185
- package/dist/exchanges/interface.d.ts +64 -8
- package/dist/exchanges/interface.js +31 -0
- package/dist/exchanges/lighter-public.d.ts +66 -11
- package/dist/exchanges/lighter-public.js +249 -81
- package/dist/exchanges/lighter-spot.d.ts +7 -1
- package/dist/exchanges/lighter-spot.js +99 -90
- package/dist/exchanges/lighter.d.ts +70 -11
- package/dist/exchanges/lighter.js +285 -92
- package/dist/exchanges/ondo/adapter.d.ts +34 -8
- package/dist/exchanges/ondo/adapter.js +73 -16
- package/dist/exchanges/ondo/public.d.ts +27 -2
- package/dist/exchanges/ondo/public.js +63 -31
- package/dist/exchanges/outcome-interface.d.ts +4 -0
- package/dist/exchanges/pacifica-spot.d.ts +6 -0
- package/dist/exchanges/pacifica-spot.js +23 -15
- package/dist/exchanges/pacifica-vaults.d.ts +9 -0
- package/dist/exchanges/pacifica-vaults.js +14 -1
- package/dist/exchanges/pacifica.d.ts +77 -6
- package/dist/exchanges/pacifica.js +321 -130
- package/dist/exchanges/public-balances.d.ts +25 -0
- package/dist/exchanges/public-balances.js +158 -0
- package/dist/exchanges/public-positions.d.ts +43 -0
- package/dist/exchanges/public-positions.js +154 -0
- package/dist/exchanges/qfex/adapter.d.ts +52 -9
- package/dist/exchanges/qfex/adapter.js +74 -9
- package/dist/exchanges/qfex/public.d.ts +62 -9
- package/dist/exchanges/qfex/public.js +128 -22
- package/dist/exchanges/qfex/ws-transport.d.ts +1 -1
- package/dist/exchanges/risex/adapter.d.ts +19 -7
- package/dist/exchanges/risex/adapter.js +58 -12
- package/dist/exchanges/spot-factory.js +1 -1
- package/dist/exchanges/txflow/adapter.d.ts +14 -7
- package/dist/exchanges/txflow/adapter.js +51 -19
- package/dist/exchanges/txflow/client.js +3 -1
- package/dist/exchanges/txflow/public.d.ts +12 -1
- package/dist/exchanges/txflow/public.js +82 -11
- package/dist/exchanges/unknown-symbol.d.ts +6 -1
- package/dist/exchanges/unknown-symbol.js +7 -2
- package/dist/exchanges/variational/adapter.d.ts +2 -6
- package/dist/exchanges/venue-types.d.ts +46 -3
- package/dist/exchanges/venue-types.js +80 -9
- package/dist/exchanges/venue.d.ts +99 -3
- package/dist/exchanges/venue.js +72 -0
- package/dist/exchanges/venues/hyperliquid.js +9 -0
- package/dist/exchanges/venues/lighter-rh.js +13 -3
- package/dist/exchanges/venues/lighter.js +13 -3
- package/dist/exchanges/venues/pacifica.js +9 -0
- package/dist/exchanges/venues/risex.js +15 -0
- package/dist/funding/legacy-public.d.ts +5 -3
- package/dist/funding/legacy-public.js +33 -37
- package/dist/funding/rates.d.ts +54 -5
- package/dist/funding/rates.js +49 -13
- package/dist/liquidity.d.ts +3 -1
- package/dist/liquidity.js +8 -4
- package/dist/mcp-pacifica-earn.js +11 -5
- package/dist/mcp-server.js +104 -15
- package/dist/pacifica/client.d.ts +2 -1
- package/dist/pacifica/client.js +11 -0
- package/dist/positions-by-address.d.ts +34 -0
- package/dist/positions-by-address.js +46 -0
- package/dist/strategies/funding-arb.js +35 -62
- package/dist/utils.d.ts +11 -1
- package/dist/venue-health.js +1 -1
- package/dist/ws/arcus-market.d.ts +9 -0
- package/dist/ws/arcus-market.js +9 -0
- package/dist/ws/aster-market.d.ts +33 -0
- package/dist/ws/aster-market.js +30 -0
- package/dist/ws/index.d.ts +30 -9
- package/dist/ws/index.js +180 -15
- package/dist/ws/lighter-market.d.ts +14 -0
- package/dist/ws/lighter-market.js +13 -0
- package/dist/ws/node.d.ts +30 -9
- package/dist/ws/node.js +204 -16
- package/dist/ws/unclassified.d.ts +16 -0
- package/dist/ws/unclassified.js +43 -0
- package/package.json +15 -2
- package/skills/perp-cli/SKILL.md +97 -33
- package/skills/perp-cli/package.json +15 -2
- package/skills/perp-cli/references/agent-operations.md +15 -12
- package/skills/perp-cli/references/commands.md +67 -12
- package/skills/perp-cli/references/strategies.md +38 -3
- package/skills/perp-cli/references/ws-library.md +55 -24
- package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
- package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
- package/skills/perp-cli/scripts/preflight.sh +27 -19
- package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
- package/skills/perp-cli/scripts/validate-arb.sh +56 -39
- package/dist/cli-spec.d.ts +0 -46
- package/dist/cli-spec.js +0 -81
|
@@ -5,12 +5,12 @@ import { tradingV2Of } from "../exchanges/legacy-trading-v2.js";
|
|
|
5
5
|
import { invalidateCache } from "../cache.js";
|
|
6
6
|
import { isDryRun, networkOf, supportsDryRun } from "../cli-flags.js";
|
|
7
7
|
import chalk from "chalk";
|
|
8
|
-
import { formatUsd, printJson, jsonOk, jsonError, makeTable,
|
|
8
|
+
import { formatUsd, printJson, jsonOk, jsonError, makeTable, symbolMatch } from "../utils.js";
|
|
9
9
|
import { computeExecutableSize } from "../liquidity.js";
|
|
10
10
|
import { computeMatchedSize, computeSpotPerpMatchedSize } from "../arb/index.js";
|
|
11
|
-
import { toHourlyRate } from "../funding.js";
|
|
11
|
+
import { getFundingHours, toHourlyRate } from "../funding.js";
|
|
12
|
+
import { fetchVenuesRates } from "../funding/rates.js";
|
|
12
13
|
import { toHourlyRateByBasis } from "../funding/normalize.js";
|
|
13
|
-
import { fetchPacificaPricesRaw, parsePacificaRaw, fetchHyperliquidMetaRaw, parseHyperliquidMetaRaw, fetchLighterOrderBookDetailsRaw, fetchLighterFundingRatesRaw, parseLighterRaw, } from "../shared-api.js";
|
|
14
14
|
import { fetchAllDexAssetsDetailed, findDexArbPairs, scanDexArb } from "../dex-asset-map.js";
|
|
15
15
|
import { logExecution, readExecutionLog } from "../execution-log.js";
|
|
16
16
|
import { smartOrder } from "../smart-order.js";
|
|
@@ -18,9 +18,23 @@ import { aggressiveSettleBoost, estimateFundingUntilSettlement, computeBasisRisk
|
|
|
18
18
|
import { checkChainMargins, marginEntryBlocks, computeAutoSize, } from "../cross-chain-margin.js";
|
|
19
19
|
import { loadArbState, addPosition as persistAddPosition, removePosition as persistRemovePosition, updatePosition as persistUpdatePosition, createInitialState, createDaemonStateStore, } from "../arb-state.js";
|
|
20
20
|
import { handleRates, handleBasisScan, handleDexScan, handleCompare, handleFundingHistory, handleFundingPositions, fetchAllPrices, printGapTable, printTrackSummary, formatGapPrice, } from "./arb.js";
|
|
21
|
+
/** The venue symbol a snapshot / position leg trades on `exchange` (the canonical key when the venue uses it). */
|
|
22
|
+
export function legSymbol(row, exchange) {
|
|
23
|
+
return row.venueSymbols?.[exchange] ?? row.symbol;
|
|
24
|
+
}
|
|
25
|
+
/** longSymbol / shortSymbol of a new position — only when a leg's venue symbol differs from the key. */
|
|
26
|
+
function legSymbolsOf(snap, longExchange, shortExchange) {
|
|
27
|
+
const l = legSymbol(snap, longExchange), sh = legSymbol(snap, shortExchange);
|
|
28
|
+
return l === snap.symbol && sh === snap.symbol ? {} : { longSymbol: l, shortSymbol: sh };
|
|
29
|
+
}
|
|
30
|
+
/** --symbols matches a row by its canonical key or any venue's own symbol (kPEPE or 1000PEPE). */
|
|
31
|
+
export function matchesSymbolFilter(row, filter) {
|
|
32
|
+
const want = new Set(filter.map(s => s.toUpperCase()));
|
|
33
|
+
return want.has(row.symbol.toUpperCase()) || Object.values(row.venueSymbols ?? {}).some(v => want.has(v.toUpperCase()));
|
|
34
|
+
}
|
|
21
35
|
// ── Fee-Adjusted Net Spread Calculation ──
|
|
22
36
|
import { getTakerFee } from "../constants.js";
|
|
23
|
-
import { assertFundingArbVenue, getVenue, listVenuesWithRole,
|
|
37
|
+
import { assertFundingArbVenue, getVenue, listVenuesWithRole, resolveVenueId, roleUnsupported, tryResolveVenueId } from "../exchanges/venue.js";
|
|
24
38
|
import { sharedApiNetwork } from "../api/public/urls.js";
|
|
25
39
|
import { findSpotQuoteBalance, readSpotQuoteAvailableFresh, requireBotSpotVenue, requireSpotAdapter, spotNetworkOf, spotPairSymbol, spotSharesPerpMargin, sweepSpotQuoteToPerp, transferQuoteToPerp, transferQuoteToSpot } from "../exchanges/spot-factory.js";
|
|
26
40
|
import { fillSizeString, readSpotBalancesFresh, sizeDecimalsOf, spotLotStepOf, spotMarketFill } from "../exchanges/spot-result.js";
|
|
@@ -380,67 +394,60 @@ export function dexCloseBlockedBy(pos, failedDexes) {
|
|
|
380
394
|
return null;
|
|
381
395
|
}
|
|
382
396
|
const _warnedArbPriceMismatch = new Set();
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
const hlRes = settled[1].status === "fulfilled" ? settled[1].value : null;
|
|
401
|
-
const ltDetailsRes = settled[2].status === "fulfilled" ? settled[2].value : null;
|
|
402
|
-
const ltFundingRes = settled[3].status === "fulfilled" ? settled[3].value : null;
|
|
403
|
-
// Both Aster calls are needed: a rate without its per-symbol interval cannot be normalized.
|
|
404
|
-
const astPremiums = settled[4].status === "fulfilled" && settled[5].status === "fulfilled"
|
|
405
|
-
? settled[4].value
|
|
406
|
-
: [];
|
|
407
|
-
const astFundingInfo = settled[5].status === "fulfilled" ? settled[5].value : [];
|
|
397
|
+
/** " (lastSettled)"-style tag of a snapshot leg's rate kind; "" when the venue is not a leg. */
|
|
398
|
+
const kindTag = (s, ex) => { const k = s.rateKinds?.[ex]; return k === undefined ? "" : `(${k ?? "?"})`; };
|
|
399
|
+
export async function fetchFundingSpreads() {
|
|
400
|
+
// The SAME rate rows as `arb scan --rates` (fetchVenuesRates over each venue's public directory):
|
|
401
|
+
// one rate per venue and canonical symbol (unclassified / ambiguous tickers and non-matchable
|
|
402
|
+
// listings have no canonical and never pair), each with its basis and rateKind. A venue that
|
|
403
|
+
// fails is reported, never silently treated as "no spread".
|
|
404
|
+
const venues = ["pacifica", "hyperliquid", "lighter", "aster"].map(id => getVenue(id));
|
|
405
|
+
const { rates: fetched, exchangeErrors } = await fetchVenuesRates(venues, sharedApiNetwork(), false);
|
|
406
|
+
// Never a leg on an unverified rate (row label; the venue filter above already drops them).
|
|
407
|
+
const rates = fetched.filter(r => r.fundingVerified);
|
|
408
|
+
for (const [ex, msg] of Object.entries(exchangeErrors))
|
|
409
|
+
process.stderr.write(`[arb-auto] ${ex}: funding rates unavailable — ${msg}\n`);
|
|
410
|
+
const pacRates = new Map(), pacPrices = new Map();
|
|
411
|
+
const hlRates = new Map(), hlPrices = new Map();
|
|
412
|
+
const ltRates = new Map(), ltPrices = new Map();
|
|
413
|
+
const astRates = new Map(), astPrices = new Map();
|
|
408
414
|
const astHoursBySym = new Map();
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const hours = Number(row.fundingIntervalHours);
|
|
412
|
-
if (rawSym.endsWith("USDT") && Number.isFinite(hours) && hours > 0)
|
|
413
|
-
astHoursBySym.set(rawSym.replace(/USDT$/, ""), hours);
|
|
414
|
-
}
|
|
415
|
-
const { rates: pacRates, prices: pacPrices } = parsePacificaRaw(pacRes);
|
|
416
|
-
const { rates: hlRates, prices: hlPrices } = parseHyperliquidMetaRaw(hlRes);
|
|
417
|
-
const { rates: ltRates, prices: ltPrices } = parseLighterRaw(ltDetailsRes, ltFundingRes);
|
|
418
|
-
// Aster: parse premiumIndex.
|
|
419
|
-
// SSOT rule #2: never silently coerce missing markPrice/lastFundingRate to 0
|
|
420
|
-
// — a 0 markPrice corrupts every downstream sizing calculation, and a 0
|
|
421
|
-
// funding rate flips the symbol off the arb radar without explanation.
|
|
422
|
-
// Skip the symbol entirely if either field is missing or non-finite.
|
|
423
|
-
const astRates = new Map();
|
|
424
|
-
const astPrices = new Map();
|
|
415
|
+
const kindsBySym = new Map();
|
|
416
|
+
const venueSymbolsBySym = new Map();
|
|
425
417
|
const astNoInterval = [];
|
|
426
|
-
for (const
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
418
|
+
for (const r of rates) {
|
|
419
|
+
const sym = r.canonical;
|
|
420
|
+
const kinds = kindsBySym.get(sym) ?? {};
|
|
421
|
+
kinds[r.exchange] = r.rateKind;
|
|
422
|
+
kindsBySym.set(sym, kinds);
|
|
423
|
+
const vs = venueSymbolsBySym.get(sym) ?? {};
|
|
424
|
+
vs[r.exchange] = r.symbol; // the venue's own symbol (orders use it)
|
|
425
|
+
venueSymbolsBySym.set(sym, vs);
|
|
426
|
+
if (r.exchange === "aster") {
|
|
427
|
+
if (r.basis.kind !== "perHours") {
|
|
428
|
+
astNoInterval.push(r.symbol);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
astRates.set(sym, r.fundingRate);
|
|
432
|
+
astPrices.set(sym, r.markPrice);
|
|
433
|
+
astHoursBySym.set(sym, r.basis.hours);
|
|
436
434
|
continue;
|
|
437
435
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
436
|
+
// The snapshot keeps each venue's rate in the venue's own basis (snapshotHourly /
|
|
437
|
+
// computeAnnualSpread normalize with it): convert from the row's hourly rate.
|
|
438
|
+
const inVenueBasis = r.hourlyRate * getFundingHours(r.exchange);
|
|
439
|
+
if (r.exchange === "pacifica") {
|
|
440
|
+
pacRates.set(sym, inVenueBasis);
|
|
441
|
+
pacPrices.set(sym, r.markPrice);
|
|
442
|
+
}
|
|
443
|
+
else if (r.exchange === "hyperliquid") {
|
|
444
|
+
hlRates.set(sym, inVenueBasis);
|
|
445
|
+
hlPrices.set(sym, r.markPrice);
|
|
446
|
+
}
|
|
447
|
+
else if (r.exchange === "lighter") {
|
|
448
|
+
ltRates.set(sym, inVenueBasis);
|
|
449
|
+
ltPrices.set(sym, r.markPrice);
|
|
441
450
|
}
|
|
442
|
-
astRates.set(sym, fundingRate);
|
|
443
|
-
astPrices.set(sym, markPrice);
|
|
444
451
|
}
|
|
445
452
|
if (astNoInterval.length > 0) {
|
|
446
453
|
process.stderr.write(`[arb-auto] aster: ${astNoInterval.length} symbol(s) excluded — no fundingIntervalHours in /fapi/v3/fundingInfo: ${astNoInterval.slice(0, 10).join(", ")}${astNoInterval.length > 10 ? ", …" : ""}\n`);
|
|
@@ -528,12 +535,54 @@ async function fetchFundingSpreads() {
|
|
|
528
535
|
ltMarkPrice: ltPrices.get(sym) ?? 0,
|
|
529
536
|
astMarkPrice: astPrices.get(sym) ?? 0,
|
|
530
537
|
astHours: ast !== undefined ? astHours : null,
|
|
538
|
+
rateKinds: Object.fromEntries(available.map(r => [r.exchange, kindsBySym.get(sym)?.[r.exchange] ?? null])),
|
|
539
|
+
venueSymbols: venueSymbolsBySym.get(sym) ?? {},
|
|
531
540
|
});
|
|
532
541
|
}
|
|
533
542
|
return snapshots.sort((a, b) => Math.abs(b.spread) - Math.abs(a.spread));
|
|
534
543
|
}
|
|
535
544
|
// Module-level holder for spot-close handler -- assigned inside registerArbAutoCommands.
|
|
536
545
|
export let handleSpotClose;
|
|
546
|
+
/**
|
|
547
|
+
* `arb auto` flag validation: every numeric flag must be a finite number in its range, `--size` a
|
|
548
|
+
* number or "auto", `--settle-strategy` one of block | aggressive | off. INVALID_PARAMS otherwise.
|
|
549
|
+
*/
|
|
550
|
+
export function assertArbAutoFlags(opts) {
|
|
551
|
+
const num = (flag, key, min, opt = {}) => {
|
|
552
|
+
const raw = opts[key];
|
|
553
|
+
if (raw === undefined)
|
|
554
|
+
return;
|
|
555
|
+
const text = String(raw).trim();
|
|
556
|
+
const v = Number(text);
|
|
557
|
+
const ok = text !== "" && Number.isFinite(v) && (opt.exclusiveMin ? v > min : v >= min) && (!opt.integer || Number.isInteger(v));
|
|
558
|
+
if (!ok) {
|
|
559
|
+
throw new PerpError("INVALID_PARAMS", `${flag} must be ${opt.integer ? "a whole number" : "a number"} ${opt.exclusiveMin ? ">" : ">="} ${min}, got "${text}"`, {
|
|
560
|
+
remediation: `Pass e.g. ${flag} ${opt.exclusiveMin ? min + 1 : Math.max(min, 1)}`,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
num("--min-spread", "minSpread", 0);
|
|
565
|
+
num("--close-spread", "closeSpread", 0);
|
|
566
|
+
if (opts.size !== undefined && String(opts.size).toLowerCase() !== "auto")
|
|
567
|
+
num("--size", "size", 0, { exclusiveMin: true });
|
|
568
|
+
num("--min-size", "minSize", 0);
|
|
569
|
+
num("--max-positions", "maxPositions", 1, { integer: true });
|
|
570
|
+
num("--interval", "interval", 0, { integer: true });
|
|
571
|
+
num("--hold-days", "holdDays", 0, { exclusiveMin: true });
|
|
572
|
+
num("--bridge-cost", "bridgeCost", 0);
|
|
573
|
+
num("--min-margin", "minMargin", 0);
|
|
574
|
+
num("--max-basis", "maxBasis", 0, { exclusiveMin: true });
|
|
575
|
+
num("--cooldown", "cooldown", 0);
|
|
576
|
+
num("--min-oi", "minOi", 0);
|
|
577
|
+
const strat = opts.settleStrategy;
|
|
578
|
+
if (strat !== undefined && !["block", "aggressive", "off"].includes(String(strat))) {
|
|
579
|
+
throw new PerpError("INVALID_PARAMS", `--settle-strategy must be block, aggressive or off, got "${String(strat)}"`, { remediation: "Pass --settle-strategy block (default)" });
|
|
580
|
+
}
|
|
581
|
+
const grade = opts.minGrade;
|
|
582
|
+
if (grade !== undefined && !["A", "B", "C", "D"].includes(String(grade).toUpperCase())) {
|
|
583
|
+
throw new PerpError("INVALID_PARAMS", `--min-grade must be A, B, C or D, got "${String(grade)}"`, { remediation: "Pass --min-grade C (default)" });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
537
586
|
export function registerArbAutoCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex) {
|
|
538
587
|
const arb = program.commands.find(c => c.name() === "arb");
|
|
539
588
|
if (!arb)
|
|
@@ -567,6 +616,9 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
567
616
|
.option("--min-oi <usd>", "Min OI (USD) to enter (hip3 mode)", "50000")
|
|
568
617
|
.option("--min-grade <grade>", "Min viability grade: A, B, C, D (hip3 mode)", "C")
|
|
569
618
|
.action(async (opts) => {
|
|
619
|
+
// Numeric flags are checked up front (also before --background starts a job): a value that is
|
|
620
|
+
// not a number ("--cooldown abc" → NaN) would otherwise silently disable the check it feeds.
|
|
621
|
+
assertArbAutoFlags(opts);
|
|
570
622
|
// Route to HIP-3 dex-auto logic if --hip3 flag is set
|
|
571
623
|
if (opts.hip3) {
|
|
572
624
|
return handleDexAuto({
|
|
@@ -647,6 +699,8 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
647
699
|
const marginUnknownNotified = new Set();
|
|
648
700
|
// Track close times per symbol to prevent close→re-entry loops
|
|
649
701
|
const closeCooldowns = new Map(); // symbol → close timestamp ms
|
|
702
|
+
// --symbols entries already reported as resolving to another canonical symbol (logged once).
|
|
703
|
+
const aliasNoted = new Set();
|
|
650
704
|
// Consecutive definite close failures per symbol (quarantined at MAX_PAIR_CLOSE_FAILURES)
|
|
651
705
|
const closeFailures = new Map();
|
|
652
706
|
// -- State Persistence: Initialize or recover --
|
|
@@ -687,7 +741,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
687
741
|
(await getAdapterForExchange(p.longExchange)).getPositions(),
|
|
688
742
|
(await getAdapterForExchange(p.shortExchange)).getPositions(),
|
|
689
743
|
]);
|
|
690
|
-
const has = (list, side) => list.some(x => symbolMatch(x.symbol, p.symbol) && x.side === side && Math.abs(Number(x.size)) > 0);
|
|
744
|
+
const has = (list, side) => list.some(x => symbolMatch(x.symbol, (side === "long" ? p.longSymbol : p.shortSymbol) ?? p.symbol) && x.side === side && Math.abs(Number(x.size)) > 0);
|
|
691
745
|
if (has(longs, "long") && has(shorts, "short")) {
|
|
692
746
|
staleSymbols.delete(p.symbol);
|
|
693
747
|
return true;
|
|
@@ -722,6 +776,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
722
776
|
entryMarkPrice: persisted.entryLongPrice,
|
|
723
777
|
accumulatedFundingUsd: persisted.accumulatedFunding,
|
|
724
778
|
lastCheckTime: new Date(persisted.lastCheckTime).getTime(),
|
|
779
|
+
longSymbol: persisted.longSymbol, shortSymbol: persisted.shortSymbol,
|
|
725
780
|
});
|
|
726
781
|
}
|
|
727
782
|
daemonState.lastStartTime = new Date().toISOString();
|
|
@@ -793,6 +848,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
793
848
|
entryMarkPrice: persisted.entryLongPrice,
|
|
794
849
|
accumulatedFundingUsd: persisted.accumulatedFunding,
|
|
795
850
|
lastCheckTime: new Date(persisted.lastCheckTime).getTime(),
|
|
851
|
+
longSymbol: persisted.longSymbol, shortSymbol: persisted.shortSymbol,
|
|
796
852
|
});
|
|
797
853
|
}
|
|
798
854
|
}
|
|
@@ -813,7 +869,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
813
869
|
try {
|
|
814
870
|
const spreads = await fetchFundingSpreads();
|
|
815
871
|
const filtered = filterSymbols
|
|
816
|
-
? spreads.filter(s =>
|
|
872
|
+
? spreads.filter(s => matchesSymbolFilter(s, filterSymbols))
|
|
817
873
|
: spreads;
|
|
818
874
|
const now = new Date().toLocaleTimeString();
|
|
819
875
|
// Check for close conditions on open positions
|
|
@@ -827,7 +883,9 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
827
883
|
// Per-position isolation: one position's failing exit check (e.g. an
|
|
828
884
|
// unknown funding basis in snapshotHourly) must not skip the others.
|
|
829
885
|
try {
|
|
830
|
-
|
|
886
|
+
// By canonical key; a position opened before 0.15.1 is keyed by its (shared) venue symbol.
|
|
887
|
+
const current = filtered.find(s => s.symbol === pos.symbol
|
|
888
|
+
|| (legSymbol(s, pos.longExchange) === pos.symbol && legSymbol(s, pos.shortExchange) === pos.symbol));
|
|
831
889
|
if (!current)
|
|
832
890
|
continue;
|
|
833
891
|
const currentSpread = Math.abs(current.spread);
|
|
@@ -884,8 +942,8 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
884
942
|
const sizeStr = String(pos.size);
|
|
885
943
|
const closeRes = await closePerpPairVerified({
|
|
886
944
|
source: "arb-auto", sym: pos.symbol,
|
|
887
|
-
longAdapter, longExch: pos.longExchange, longSym: pos.symbol, longSize: sizeStr,
|
|
888
|
-
shortAdapter, shortExch: pos.shortExchange, shortSym: pos.symbol, shortSize: sizeStr, smart: false,
|
|
945
|
+
longAdapter, longExch: pos.longExchange, longSym: pos.longSymbol ?? pos.symbol, longSize: sizeStr,
|
|
946
|
+
shortAdapter, shortExch: pos.shortExchange, shortSym: pos.shortSymbol ?? pos.symbol, shortSize: sizeStr, smart: false,
|
|
889
947
|
log: (m) => console.log(chalk.red(m)),
|
|
890
948
|
});
|
|
891
949
|
if (closeRes.status === "closed") {
|
|
@@ -1098,8 +1156,15 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1098
1156
|
}
|
|
1099
1157
|
};
|
|
1100
1158
|
for (const sym of filterSymbols ?? []) {
|
|
1101
|
-
|
|
1159
|
+
// A --symbols entry may be a venue form (kPEPE): it monitors the canonical row it resolves to.
|
|
1160
|
+
const hit = filtered.find(x => matchesSymbolFilter(x, [sym]));
|
|
1161
|
+
if (hit === undefined)
|
|
1102
1162
|
console.log(chalk.gray(` ${now} SKIP ${sym}: not in this cycle's funding scan (no rate on two compared venues)`));
|
|
1163
|
+
else if (hit.symbol !== sym && !aliasNoted.has(sym)) {
|
|
1164
|
+
aliasNoted.add(sym);
|
|
1165
|
+
const forms = Object.entries(hit.venueSymbols ?? {}).map(([v, vs]) => `${v} ${vs}`).join(" / ");
|
|
1166
|
+
console.log(chalk.gray(` ${now} MONITOR ${sym} → ${hit.symbol} (canonical${forms !== "" ? `; ${forms}` : ""})`));
|
|
1167
|
+
}
|
|
1103
1168
|
}
|
|
1104
1169
|
if (openPositions.length >= maxPositions) {
|
|
1105
1170
|
console.log(chalk.gray(` ${now} NO ENTRY: max positions reached (${openPositions.length}/${maxPositions}: ${openPositions.map(p => `${p.symbol} ${p.longExchange}/${p.shortExchange}`).join(", ")})`));
|
|
@@ -1195,7 +1260,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1195
1260
|
try {
|
|
1196
1261
|
const longAdapter = await getAdapterForExchange(longExchange);
|
|
1197
1262
|
const shortAdapter = await getAdapterForExchange(shortExchange);
|
|
1198
|
-
actualSizeUsd = await computeAutoSize(longAdapter, shortAdapter, snap
|
|
1263
|
+
actualSizeUsd = await computeAutoSize(longAdapter, shortAdapter, legSymbol(snap, longExchange), 0.3, legSymbol(snap, shortExchange));
|
|
1199
1264
|
if (actualSizeUsd <= 0) {
|
|
1200
1265
|
console.log(chalk.gray(` ${now} SKIP ${snap.symbol}: auto-size returned $0 (insufficient depth/margin)`));
|
|
1201
1266
|
continue;
|
|
@@ -1239,7 +1304,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1239
1304
|
legsStarted = true;
|
|
1240
1305
|
const res = await openPerpPairVerified({
|
|
1241
1306
|
source: "arb-auto", sym: snap.symbol,
|
|
1242
|
-
longAdapter, longExch: longExchange, longSym: snap
|
|
1307
|
+
longAdapter, longExch: longExchange, longSym: legSymbol(snap, longExchange), shortAdapter, shortExch: shortExchange, shortSym: legSymbol(snap, shortExchange),
|
|
1243
1308
|
size: sizeInAsset, smart: false, log: (m) => console.log(chalk.red(m)),
|
|
1244
1309
|
});
|
|
1245
1310
|
legsStarted = false; // outcome known (opened / not opened / quarantined)
|
|
@@ -1268,6 +1333,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1268
1333
|
entryShortPrice: snap.markPrice,
|
|
1269
1334
|
accumulatedFunding: 0,
|
|
1270
1335
|
lastCheckTime: entryTime,
|
|
1336
|
+
...legSymbolsOf(snap, longExchange, shortExchange),
|
|
1271
1337
|
});
|
|
1272
1338
|
}
|
|
1273
1339
|
catch (e) {
|
|
@@ -1291,6 +1357,9 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1291
1357
|
});
|
|
1292
1358
|
console.error(chalk.red(` ${now} ENTRY FAILED ${snap.symbol}: ${res.reason}${res.rolledBack !== null ? ` — rolled back long ${res.rolledBack}; no open exposure` : " — nothing opened"}`));
|
|
1293
1359
|
donePending();
|
|
1360
|
+
// Cool down like a close: a leg the venue rejects (unknown symbol, lot, margin)
|
|
1361
|
+
// is not re-sent — with its fees and rollback — every cycle.
|
|
1362
|
+
closeCooldowns.set(snap.symbol, Date.now());
|
|
1294
1363
|
continue;
|
|
1295
1364
|
}
|
|
1296
1365
|
else {
|
|
@@ -1341,6 +1410,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1341
1410
|
entryMarkPrice: snap.markPrice,
|
|
1342
1411
|
accumulatedFundingUsd: 0,
|
|
1343
1412
|
lastCheckTime: Date.now(),
|
|
1413
|
+
...legSymbolsOf(snap, longExchange, shortExchange),
|
|
1344
1414
|
});
|
|
1345
1415
|
// Dry-run: record the simulated entry in the in-memory state only (never the real file).
|
|
1346
1416
|
if (dryRun) {
|
|
@@ -1594,7 +1664,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1594
1664
|
const topCheck = filtered.slice(0, 5);
|
|
1595
1665
|
await Promise.allSettled(topCheck.map(async (s) => {
|
|
1596
1666
|
try {
|
|
1597
|
-
const [hlOB, ltOB] = await Promise.all([fetchHLOrderbook(s
|
|
1667
|
+
const [hlOB, ltOB] = await Promise.all([fetchHLOrderbook(legSymbol(s, "hyperliquid")), fetchLighterOrderbook(legSymbol(s, "lighter"))]);
|
|
1598
1668
|
const hlDepth = hlOB.reduce((sum, l) => sum + l[0] * l[1], 0);
|
|
1599
1669
|
const ltDepth = ltOB.reduce((sum, l) => sum + l[0] * l[1], 0);
|
|
1600
1670
|
const hlBest = hlOB[0]?.[0] ?? 0;
|
|
@@ -1613,12 +1683,13 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1613
1683
|
const grossColor = grossSpread >= 50 ? chalk.green.bold : grossSpread >= 30 ? chalk.green : chalk.yellow;
|
|
1614
1684
|
const netColor = netSpread >= 20 ? chalk.green : netSpread >= 0 ? chalk.yellow : chalk.red;
|
|
1615
1685
|
const rates = [];
|
|
1686
|
+
// Each leg names which rate it is (lastSettled / accruing / predicted; ? = unverified).
|
|
1616
1687
|
if (s.pacRate)
|
|
1617
|
-
rates.push(`PAC:${(s.pacRate * 100).toFixed(4)}
|
|
1688
|
+
rates.push(`PAC:${(s.pacRate * 100).toFixed(4)}%${kindTag(s, "pacifica")}`);
|
|
1618
1689
|
if (s.hlRate)
|
|
1619
|
-
rates.push(`HL:${(s.hlRate * 100).toFixed(4)}
|
|
1690
|
+
rates.push(`HL:${(s.hlRate * 100).toFixed(4)}%${kindTag(s, "hyperliquid")}`);
|
|
1620
1691
|
if (s.ltRate)
|
|
1621
|
-
rates.push(`LT:${(s.ltRate * 100).toFixed(4)}
|
|
1692
|
+
rates.push(`LT:${(s.ltRate * 100).toFixed(4)}%${kindTag(s, "lighter")}`);
|
|
1622
1693
|
let liqInfo = "";
|
|
1623
1694
|
const ld = liqData.get(s.symbol);
|
|
1624
1695
|
if (ld)
|
|
@@ -1682,8 +1753,10 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1682
1753
|
const netSpread = computeNetSpread(s.annualSpreadPct, holdDays, rtCost, effectiveBridgeCost, sizeUsd);
|
|
1683
1754
|
return {
|
|
1684
1755
|
mode: "spot-perp", symbol: s.symbol, perpExchange: s.perpExchange,
|
|
1685
|
-
|
|
1686
|
-
|
|
1756
|
+
// grossSpread / netSpread: the names every other arb scan row uses (same % values);
|
|
1757
|
+
// the *Pct names stay for existing consumers.
|
|
1758
|
+
spotExchanges: s.spotExchanges, grossSpreadPct: s.annualSpreadPct, grossSpread: s.annualSpreadPct,
|
|
1759
|
+
netSpreadPct: netSpread, netSpread, estFeesPct: rtCost,
|
|
1687
1760
|
perpFundingRate: s.perpFundingRate, direction: s.direction,
|
|
1688
1761
|
markPrice: s.bestMarkPrice, estHourlyIncomeUsd: s.estHourlyIncomeUsd,
|
|
1689
1762
|
breakEvenDays: rtCost > 0 ? Math.ceil(rtCost / (s.annualSpreadPct / 365)) : 0,
|
|
@@ -1747,7 +1820,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1747
1820
|
const shortPrice = pFor(s.shortExch);
|
|
1748
1821
|
const mid = (longPrice + shortPrice) / 2;
|
|
1749
1822
|
const priceGapPct = mid > 0 ? Math.abs(longPrice - shortPrice) / mid * 100 : 0;
|
|
1750
|
-
return { mode: "perp-perp", symbol: s.symbol, longExch: s.longExch, shortExch: s.shortExch, markPrice: s.markPrice, grossSpread, netSpread: net, estFeesPct: rtCost, priceGapPct: Math.round(priceGapPct * 10000) / 10000 };
|
|
1823
|
+
return { mode: "perp-perp", symbol: s.symbol, longExch: s.longExch, shortExch: s.shortExch, markPrice: s.markPrice, grossSpread, grossSpreadPct: grossSpread, netSpread: net, netSpreadPct: net, estFeesPct: rtCost, priceGapPct: Math.round(priceGapPct * 10000) / 10000 };
|
|
1751
1824
|
});
|
|
1752
1825
|
// Merge spot-perp results if "all" mode
|
|
1753
1826
|
const spotPerp = (opts.mode === "all" && spotSpreads) ? spotSpreads.map(s => {
|
|
@@ -1757,8 +1830,8 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1757
1830
|
const net = computeNetSpread(s.annualSpreadPct, holdDays, rtCost, effectiveBridgeCost, sizeUsd);
|
|
1758
1831
|
return {
|
|
1759
1832
|
mode: "spot-perp", symbol: s.symbol, perpExchange: s.perpExchange,
|
|
1760
|
-
spotExchanges: s.spotExchanges, grossSpread: s.annualSpreadPct,
|
|
1761
|
-
netSpread: net, estFeesPct: rtCost,
|
|
1833
|
+
spotExchanges: s.spotExchanges, grossSpread: s.annualSpreadPct, grossSpreadPct: s.annualSpreadPct,
|
|
1834
|
+
netSpread: net, netSpreadPct: net, estFeesPct: rtCost,
|
|
1762
1835
|
perpFundingRate: s.perpFundingRate, direction: s.direction,
|
|
1763
1836
|
markPrice: s.bestMarkPrice, breakEvenDays: rtCost > 0 ? Math.ceil(rtCost / (s.annualSpreadPct / 365)) : 0,
|
|
1764
1837
|
};
|
|
@@ -1787,14 +1860,15 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1787
1860
|
const priceGapPct = mid > 0 ? Math.abs(longPrice - shortPrice) / mid * 100 : 0;
|
|
1788
1861
|
const gapColor = priceGapPct >= 0.5 ? chalk.red : priceGapPct >= 0.1 ? chalk.yellow : chalk.gray;
|
|
1789
1862
|
const rates = [];
|
|
1863
|
+
// Each leg names which rate it is (lastSettled / accruing / predicted; ? = unverified).
|
|
1790
1864
|
if (s.pacRate)
|
|
1791
|
-
rates.push(`PAC:${(s.pacRate * 100).toFixed(4)}
|
|
1865
|
+
rates.push(`PAC:${(s.pacRate * 100).toFixed(4)}%${kindTag(s, "pacifica")}`);
|
|
1792
1866
|
if (s.hlRate)
|
|
1793
|
-
rates.push(`HL:${(s.hlRate * 100).toFixed(4)}
|
|
1867
|
+
rates.push(`HL:${(s.hlRate * 100).toFixed(4)}%${kindTag(s, "hyperliquid")}`);
|
|
1794
1868
|
if (s.ltRate)
|
|
1795
|
-
rates.push(`LT:${(s.ltRate * 100).toFixed(4)}
|
|
1869
|
+
rates.push(`LT:${(s.ltRate * 100).toFixed(4)}%${kindTag(s, "lighter")}`);
|
|
1796
1870
|
if (s.astRate)
|
|
1797
|
-
rates.push(`AST:${(s.astRate * 100).toFixed(4)}
|
|
1871
|
+
rates.push(`AST:${(s.astRate * 100).toFixed(4)}%${kindTag(s, "aster")}`);
|
|
1798
1872
|
return [
|
|
1799
1873
|
chalk.white.bold(s.symbol),
|
|
1800
1874
|
grossColor(`${grossSpread.toFixed(1)}%`),
|
package/dist/commands/arb.d.ts
CHANGED
|
@@ -13,6 +13,14 @@ export interface PriceSnapshot {
|
|
|
13
13
|
expensive: string;
|
|
14
14
|
}
|
|
15
15
|
export declare function fetchAllPrices(): Promise<PriceSnapshot[]>;
|
|
16
|
+
/**
|
|
17
|
+
* Cross-venue price gaps by venue symbol. The same ticker on two venues is not proof of the same
|
|
18
|
+
* instrument (Pacifica SAMSUNG 165.8 USD vs Lighter SAMSUNG 220,597 — a KRW-quoted listing): each
|
|
19
|
+
* symbol's prices go through the funding scan's median price-outlier guard (dropPriceOutliers,
|
|
20
|
+
* MAX_PRICE_DEVIATION_PCT, one warning per symbol / venue) before a gap is computed, so a gap is
|
|
21
|
+
* only reported between prices that agree within the guard.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildGapSnapshots(pacPrices: Map<string, number>, hlPrices: Map<string, number>, ltPrices: Map<string, number>): PriceSnapshot[];
|
|
16
24
|
export declare function formatGapPrice(p: number): string;
|
|
17
25
|
export declare function printGapTable(snapshots: PriceSnapshot[], minGap: number): void;
|
|
18
26
|
export declare function printTrackSummary(symbol: string, samples: {
|
package/dist/commands/arb.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { makeTable, formatUsd, formatPercent, formatPnl, printJson, jsonOk, withJsonErrors, logSettledRejections } from "../utils.js";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { toHourlyRate, annualizeHourlyRate, estimateHourlyFundingFromHourly } from "../funding.js";
|
|
4
|
-
import { fetchAllFundingRates, fetchCanonicalPriceGroups, fetchSymbolFundingRates, TOP_SYMBOLS, } from "../funding-rates.js";
|
|
4
|
+
import { fetchAllFundingRates, fetchCanonicalPriceGroups, dropPriceOutliers, fetchSymbolFundingRates, isRankedComparison, TOP_SYMBOLS, } from "../funding-rates.js";
|
|
5
5
|
import { saveFundingSnapshot, getHistoricalRates, getCompoundedAnnualReturn, getExchangeCompoundingHours, } from "../funding-history.js";
|
|
6
6
|
import { getVenue, listVenuesWithRole, tryResolveVenueId } from "../exchanges/venue.js";
|
|
7
7
|
import "../exchanges/venues/index.js";
|
|
@@ -46,23 +46,34 @@ export async function fetchAllPrices() {
|
|
|
46
46
|
ltPrices.set(sym, price);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
+
return buildGapSnapshots(pacPrices, hlPrices, ltPrices);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Cross-venue price gaps by venue symbol. The same ticker on two venues is not proof of the same
|
|
53
|
+
* instrument (Pacifica SAMSUNG 165.8 USD vs Lighter SAMSUNG 220,597 — a KRW-quoted listing): each
|
|
54
|
+
* symbol's prices go through the funding scan's median price-outlier guard (dropPriceOutliers,
|
|
55
|
+
* MAX_PRICE_DEVIATION_PCT, one warning per symbol / venue) before a gap is computed, so a gap is
|
|
56
|
+
* only reported between prices that agree within the guard.
|
|
57
|
+
*/
|
|
58
|
+
export function buildGapSnapshots(pacPrices, hlPrices, ltPrices) {
|
|
49
59
|
const allSymbols = new Set([...pacPrices.keys(), ...hlPrices.keys(), ...ltPrices.keys()]);
|
|
50
60
|
const snapshots = [];
|
|
61
|
+
const LABEL = { pacifica: "PAC", hyperliquid: "HL", lighter: "LT" };
|
|
51
62
|
for (const sym of allSymbols) {
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (hl !== null)
|
|
60
|
-
available.push({ ex: "HL", price: hl });
|
|
61
|
-
if (lt !== null)
|
|
62
|
-
available.push({ ex: "LT", price: lt });
|
|
63
|
-
if (available.length < 2)
|
|
63
|
+
const listed = [];
|
|
64
|
+
for (const [exchange, map] of [["pacifica", pacPrices], ["hyperliquid", hlPrices], ["lighter", ltPrices]]) {
|
|
65
|
+
const p = map.get(sym);
|
|
66
|
+
if (p !== undefined)
|
|
67
|
+
listed.push({ exchange, markPrice: p });
|
|
68
|
+
}
|
|
69
|
+
if (listed.length < 2)
|
|
64
70
|
continue;
|
|
65
|
-
|
|
71
|
+
const kept = dropPriceOutliers(sym, listed, "gaps");
|
|
72
|
+
// Need at least 2 exchanges whose prices agree
|
|
73
|
+
if (kept.length < 2)
|
|
74
|
+
continue;
|
|
75
|
+
const priceOf = (ex) => kept.find(k => k.exchange === ex)?.markPrice ?? null;
|
|
76
|
+
const available = kept.map(k => ({ ex: LABEL[k.exchange], price: k.markPrice })).sort((a, b) => a.price - b.price);
|
|
66
77
|
const cheapest = available[0];
|
|
67
78
|
const expensive = available[available.length - 1];
|
|
68
79
|
const maxGap = expensive.price - cheapest.price;
|
|
@@ -70,9 +81,9 @@ export async function fetchAllPrices() {
|
|
|
70
81
|
const maxGapPct = mid > 0 ? (maxGap / mid) * 100 : 0;
|
|
71
82
|
snapshots.push({
|
|
72
83
|
symbol: sym,
|
|
73
|
-
pacPrice:
|
|
74
|
-
hlPrice:
|
|
75
|
-
ltPrice:
|
|
84
|
+
pacPrice: priceOf("pacifica"),
|
|
85
|
+
hlPrice: priceOf("hyperliquid"),
|
|
86
|
+
ltPrice: priceOf("lighter"),
|
|
76
87
|
maxGap,
|
|
77
88
|
maxGapPct,
|
|
78
89
|
cheapest: cheapest.ex,
|
|
@@ -194,7 +205,8 @@ function printSnapshotTable(snapshot) {
|
|
|
194
205
|
}
|
|
195
206
|
const rows = snapshot.symbols.map(s => {
|
|
196
207
|
const rateOf = (id) => s.rates.find(r => r.exchange === id);
|
|
197
|
-
const
|
|
208
|
+
const spread = s.maxSpreadAnnual;
|
|
209
|
+
const spreadColor = spread === null ? chalk.gray : spread >= 30 ? chalk.green : spread >= 10 ? chalk.yellow : chalk.white;
|
|
198
210
|
// Historical-average source preference: HL, PAC, LT, AST, then whichever venue quoted.
|
|
199
211
|
const bestRate = rateOf("hyperliquid") ?? rateOf("pacifica") ?? rateOf("lighter") ?? rateOf("aster") ?? s.rates[0];
|
|
200
212
|
const trend = trendMap.get(s.symbol) ?? chalk.gray("-");
|
|
@@ -204,8 +216,10 @@ function printSnapshotTable(snapshot) {
|
|
|
204
216
|
const r = rateOf(v.id);
|
|
205
217
|
return r ? formatPercent(r.fundingRate) : chalk.gray("-");
|
|
206
218
|
}),
|
|
207
|
-
|
|
208
|
-
|
|
219
|
+
// No ranked pair (< 2 verified rows): unverified rates are shown, never ranked.
|
|
220
|
+
spread === null ? chalk.gray("n/a") : spreadColor(`${spread.toFixed(1)}%`), trend,
|
|
221
|
+
spread === null || s.longExchange === null || s.shortExchange === null ? chalk.gray("unranked (unverified)")
|
|
222
|
+
: spread >= 5 ? `${exAbbr(s.shortExchange)}>${exAbbr(s.longExchange)}${s.executable ? "" : chalk.gray(s.readOnlyLegs.length > 0 ? " (read-only)" : " (unverified)")}` : chalk.gray("-"),
|
|
209
223
|
formatAvgRate(bestRate?.historicalAvg?.avg8h), formatAvgRate(bestRate?.historicalAvg?.avg24h), formatAvgRate(bestRate?.historicalAvg?.avg7d),
|
|
210
224
|
];
|
|
211
225
|
});
|
|
@@ -232,9 +246,16 @@ function printDetailedComparison(comparison) {
|
|
|
232
246
|
const color = r.fundingRate > 0 ? chalk.red : r.fundingRate < 0 ? chalk.green : chalk.white;
|
|
233
247
|
console.log(` ${chalk.white.bold(exAbbr(r.exchange).padEnd(4))} Raw: ${color(formatPercent(r.fundingRate).padEnd(14))} Hourly: ${(r.hourlyRate * 100).toFixed(6)}% Annual: ${r.annualizedPct.toFixed(2)}% APY: ${(compoundedReturn * 100).toFixed(2)}%`);
|
|
234
248
|
}
|
|
249
|
+
if (!isRankedComparison(comparison)) {
|
|
250
|
+
console.log(chalk.yellow(`\n No ranked pair: fewer than two venues with verified funding (${comparison.unverifiedLegs.join(", ")} unverified — display only, never a long / short leg).\n`));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
235
253
|
const spreadColor = comparison.maxSpreadAnnual >= 30 ? chalk.green.bold : comparison.maxSpreadAnnual >= 10 ? chalk.yellow : chalk.white;
|
|
236
254
|
console.log(`\n Max Spread: ${spreadColor(`${comparison.maxSpreadAnnual.toFixed(1)}%`)} annual`);
|
|
237
|
-
console.log(` Direction: Long ${exAbbr(comparison.longExchange)} / Short ${exAbbr(comparison.shortExchange)}${comparison.executable ? "" : chalk.yellow(` — not executable: ${
|
|
255
|
+
console.log(` Direction: Long ${exAbbr(comparison.longExchange)} / Short ${exAbbr(comparison.shortExchange)}${comparison.executable ? "" : chalk.yellow(` — not executable: ${[
|
|
256
|
+
...comparison.readOnlyLegs.map(v => `${v} is read-only in perp-cli`),
|
|
257
|
+
...comparison.unverifiedLegs.map(v => `${v} funding is unverified (display only)`),
|
|
258
|
+
].join("; ")}`)}`);
|
|
238
259
|
console.log(` Est. Income: $${comparison.estHourlyIncomeUsd.toFixed(4)}/hr per $1K notional\n`);
|
|
239
260
|
}
|
|
240
261
|
// registerFundingSubcommands removed — use 'arb scan --rates/--compare/--history/--positions' instead
|
|
@@ -287,12 +308,17 @@ export async function handleRates(isJson, opts) {
|
|
|
287
308
|
const history = getHistoricalRates(s.symbol, bestEx, h24ago, now);
|
|
288
309
|
return { ...s, rateHistory: history.map(h => ({ ts: h.ts, hourlyRate: h.hourlyRate })) };
|
|
289
310
|
});
|
|
311
|
+
// --min never hides rows silently: the threshold and the hidden count are in meta (and data).
|
|
312
|
+
const minMeta = { minSpread: snapshot.minSpread, belowMinSpread: snapshot.belowMinSpread };
|
|
290
313
|
if (process.argv.includes("--ndjson") || process.argv.includes("--fields"))
|
|
291
|
-
return printJson(jsonOk(enriched));
|
|
292
|
-
return printJson(jsonOk({ ...snapshot, symbols: enriched }));
|
|
314
|
+
return printJson(jsonOk(enriched, minMeta));
|
|
315
|
+
return printJson(jsonOk({ ...snapshot, symbols: enriched }, minMeta));
|
|
293
316
|
}
|
|
294
317
|
printSnapshotTable(snapshot);
|
|
295
318
|
printFundingExchangeStatus(snapshot);
|
|
319
|
+
if (snapshot.belowMinSpread > 0) {
|
|
320
|
+
console.log(chalk.gray(` ${snapshot.belowMinSpread} pair${snapshot.belowMinSpread === 1 ? "" : "s"} below --min ${snapshot.minSpread}% hidden (show them: --min 0)\n`));
|
|
321
|
+
}
|
|
296
322
|
}
|
|
297
323
|
/**
|
|
298
324
|
* Cross-venue price basis — a PRICE comparison, independent of funding.
|