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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `perp account positions --address` and MCP get_positions_by_address: one read path over the
|
|
3
|
+
* registry (VenuePublicData.fetchPositionsByAddress). Keyless; nothing is signed.
|
|
4
|
+
*/
|
|
5
|
+
import { PerpError } from "./errors.js";
|
|
6
|
+
import { fetchPositionsByAddress, getVenue, listPublicPositionVenues } from "./exchanges/venue.js";
|
|
7
|
+
import { bareSymbolOf } from "./commands/market.js";
|
|
8
|
+
/** Rows of `symbol` (plain asset; HIP-3 "xyz:GOLD" also matches "GOLD"; "BTC-PERP" matches "BTC"). */
|
|
9
|
+
export function filterPositionsBySymbol(rows, symbol) {
|
|
10
|
+
if (symbol === undefined)
|
|
11
|
+
return rows;
|
|
12
|
+
const want = bareSymbolOf(symbol).toUpperCase();
|
|
13
|
+
return rows.filter(r => {
|
|
14
|
+
const s = r.symbol.toUpperCase();
|
|
15
|
+
return s === want || bareSymbolOf(r.symbol).toUpperCase() === want || s.split(":").pop() === want;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function warningsOf(venue, errors) {
|
|
19
|
+
return Object.entries(errors).map(([src, e]) => `${venue} ${src} unavailable (${e.code}: ${e.message}) — its positions are missing from this answer`);
|
|
20
|
+
}
|
|
21
|
+
export async function readPositionsByAddress(venue, net, address, opts = {}) {
|
|
22
|
+
const id = getVenue(venue).id;
|
|
23
|
+
const r = await fetchPositionsByAddress(id, net, address, { accountIndex: opts.accountIndex });
|
|
24
|
+
return { venue: id, positions: filterPositionsBySymbol(r.positions, opts.symbol), errors: r.errors, notes: r.notes, warnings: warningsOf(id, r.errors) };
|
|
25
|
+
}
|
|
26
|
+
/** Every venue that reads positions by address; a venue that fails (address format included) is an `errors` entry. */
|
|
27
|
+
export async function readPositionsByAddressAll(net, address, opts = {}) {
|
|
28
|
+
const venues = listPublicPositionVenues().map(v => v.id);
|
|
29
|
+
const out = { venues: {}, errors: {}, notes: {}, warnings: [] };
|
|
30
|
+
const settled = await Promise.allSettled(venues.map(v => readPositionsByAddress(v, net, address, opts.accountIndex !== undefined && v !== "lighter" && v !== "lighter-rh" ? { symbol: opts.symbol } : opts)));
|
|
31
|
+
settled.forEach((res, i) => {
|
|
32
|
+
const v = venues[i];
|
|
33
|
+
if (res.status === "rejected") {
|
|
34
|
+
const e = res.reason;
|
|
35
|
+
out.errors[v] = e instanceof PerpError ? { code: e.structured.code, message: e.structured.message } : { code: "EXCHANGE_ERROR", message: e instanceof Error ? e.message : String(e) };
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
out.venues[v] = res.value.positions;
|
|
39
|
+
for (const [src, e] of Object.entries(res.value.errors))
|
|
40
|
+
out.errors[`${v}:${src}`] = e;
|
|
41
|
+
if (res.value.notes.length > 0)
|
|
42
|
+
out.notes[v] = res.value.notes;
|
|
43
|
+
out.warnings.push(...res.value.warnings);
|
|
44
|
+
});
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { assertCanClosePositions } from "../exchanges/trading-v2.js";
|
|
2
2
|
import { MAX_PRICE_DEVIATION_PCT } from "../exchanges/index.js";
|
|
3
|
-
import { symbolMatch
|
|
3
|
+
import { symbolMatch } from "../utils.js";
|
|
4
4
|
import { logExecution } from "../execution-log.js";
|
|
5
5
|
import { updateJobState } from "../jobs.js";
|
|
6
6
|
import { invalidateCache } from "../cache.js";
|
|
7
7
|
import { fetchBalancesWithFailures, computeRebalancePlan, hasEnoughBalance, marketLeverageOf, requiredArbMargin } from "../rebalance.js";
|
|
8
8
|
import { checkArbLiquidity } from "../liquidity.js";
|
|
9
|
-
import { computeAnnualSpread } from "../funding.js";
|
|
10
|
-
import {
|
|
9
|
+
import { computeAnnualSpread, getFundingHours } from "../funding.js";
|
|
10
|
+
import { fetchVenuesRates } from "../funding/rates.js";
|
|
11
|
+
import { getVenue } from "../exchanges/venue.js";
|
|
12
|
+
import { sharedApiNetwork } from "../api/public/urls.js";
|
|
11
13
|
import { computeMatchedSize } from "../arb-sizing.js";
|
|
12
14
|
import { closePerpPairVerified, openPerpPairVerified } from "../commands/arb-auto.js";
|
|
13
15
|
import { closePerpVerified, isOutcomeUnknown } from "../bot/perp-order.js";
|
|
@@ -21,54 +23,17 @@ async function sizeDecimalsOf(adapter, symbol) {
|
|
|
21
23
|
return m === undefined ? null : m.sizeDecimals;
|
|
22
24
|
}
|
|
23
25
|
async function fetchAllRates() {
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// stderr (explicit
|
|
27
|
-
const [
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const [details, funding] = await Promise.all([
|
|
36
|
-
fetchLighterOrderBookDetails(),
|
|
37
|
-
fetchLighterFundingRates(),
|
|
38
|
-
]);
|
|
39
|
-
const priceMap = new Map(details.map(d => [d.marketId, d.lastTradePrice]));
|
|
40
|
-
const symMap = new Map(details.map(d => [d.marketId, d.symbol]));
|
|
41
|
-
const out = [];
|
|
42
|
-
for (const fr of funding) {
|
|
43
|
-
const symbol = fr.symbol || symMap.get(fr.marketId) || "";
|
|
44
|
-
if (!symbol)
|
|
45
|
-
continue;
|
|
46
|
-
// Prefer fr.markPrice; fall back to orderBook last-trade as documented
|
|
47
|
-
// price source preference (NOT an error fallback).
|
|
48
|
-
const directMark = fr.markPrice;
|
|
49
|
-
const fallbackMark = priceMap.get(fr.marketId);
|
|
50
|
-
let markPrice;
|
|
51
|
-
if (directMark !== null && Number.isFinite(directMark) && directMark > 0) {
|
|
52
|
-
markPrice = directMark;
|
|
53
|
-
}
|
|
54
|
-
else if (fallbackMark !== undefined && Number.isFinite(fallbackMark) && fallbackMark > 0) {
|
|
55
|
-
markPrice = fallbackMark;
|
|
56
|
-
}
|
|
57
|
-
// SSOT rule #2: skip rows whose mark price cannot be resolved at all
|
|
58
|
-
// — never publish a 0 mark price downstream.
|
|
59
|
-
if (markPrice === undefined)
|
|
60
|
-
continue;
|
|
61
|
-
out.push({ exchange: "lighter", symbol, fundingRate: fr.rate, markPrice });
|
|
62
|
-
}
|
|
63
|
-
return out;
|
|
64
|
-
})(),
|
|
65
|
-
]);
|
|
66
|
-
logSettledRejections([pacRates, hlRates, ltRates], ["pacifica", "hyperliquid", "lighter"], "funding-arb");
|
|
67
|
-
return [
|
|
68
|
-
...(pacRates.status === "fulfilled" ? pacRates.value : []),
|
|
69
|
-
...(hlRates.status === "fulfilled" ? hlRates.value : []),
|
|
70
|
-
...(ltRates.status === "fulfilled" ? ltRates.value : []),
|
|
71
|
-
];
|
|
26
|
+
// The SAME rate rows as `arb scan --rates` (fetchVenuesRates): one per venue and canonical
|
|
27
|
+
// symbol (unclassified / ambiguous tickers have no canonical and never pair). Each venue is
|
|
28
|
+
// independent; a failed one is reported on stderr (explicit, never silent).
|
|
29
|
+
const venues = ["pacifica", "hyperliquid", "lighter"].map(id => getVenue(id));
|
|
30
|
+
const { rates: fetched, exchangeErrors } = await fetchVenuesRates(venues, sharedApiNetwork(), false);
|
|
31
|
+
// Never a leg on an unverified rate (row label; the venue filter already drops them).
|
|
32
|
+
const rates = fetched.filter(r => r.fundingVerified);
|
|
33
|
+
for (const [ex, msg] of Object.entries(exchangeErrors))
|
|
34
|
+
process.stderr.write(`[funding-arb] ${ex}: funding rates unavailable — ${msg}\n`);
|
|
35
|
+
// computeAnnualSpread / toHourlyRate normalize with the venue's basis: keep each rate in it.
|
|
36
|
+
return rates.map(r => ({ exchange: r.exchange, symbol: r.canonical, venueSymbol: r.symbol, fundingRate: r.hourlyRate * getFundingHours(r.exchange), markPrice: r.markPrice }));
|
|
72
37
|
}
|
|
73
38
|
// annualize: use computeAnnualSpread from funding.ts for cross-exchange comparison
|
|
74
39
|
/**
|
|
@@ -111,8 +76,12 @@ export async function runFundingArb(adapters, params, jobId, log = console.log)
|
|
|
111
76
|
const rates = await fetchAllRates();
|
|
112
77
|
// Group by symbol
|
|
113
78
|
const rateMap = new Map();
|
|
79
|
+
// --symbols selects an asset by its canonical key or any venue's own symbol (kPEPE selects
|
|
80
|
+
// the whole 1000PEPE group, Lighter's leg included).
|
|
81
|
+
const want = new Set((params.symbols ?? []).map(s => s.toUpperCase()));
|
|
82
|
+
const wanted = new Set(rates.filter(r => want.has(r.symbol.toUpperCase()) || want.has(r.venueSymbol.toUpperCase())).map(r => r.symbol));
|
|
114
83
|
for (const r of rates) {
|
|
115
|
-
if (
|
|
84
|
+
if (want.size > 0 && !wanted.has(r.symbol))
|
|
116
85
|
continue;
|
|
117
86
|
if (!rateMap.has(r.symbol))
|
|
118
87
|
rateMap.set(r.symbol, []);
|
|
@@ -233,6 +202,8 @@ export async function runFundingArb(adapters, params, jobId, log = console.log)
|
|
|
233
202
|
if (annualSpread >= params.minSpread) {
|
|
234
203
|
const longEx = lowest.exchange;
|
|
235
204
|
const shortEx = highest.exchange;
|
|
205
|
+
const longSym = lowest.venueSymbol;
|
|
206
|
+
const shortSym = highest.venueSymbol;
|
|
236
207
|
log(`[ARB] ENTRY signal: ${symbol} spread ${annualSpread.toFixed(1)}% — long ${longEx} (${(lowest.fundingRate * 100).toFixed(4)}%) / short ${shortEx} (${(highest.fundingRate * 100).toFixed(4)}%)`);
|
|
237
208
|
if (params.autoExecute) {
|
|
238
209
|
const longAdapter = adapters.get(longEx);
|
|
@@ -243,7 +214,7 @@ export async function runFundingArb(adapters, params, jobId, log = console.log)
|
|
|
243
214
|
}
|
|
244
215
|
// Check orderbook liquidity & adjust size
|
|
245
216
|
const requestedUsd = params.sizeUsd ?? Number(params.size) * highest.markPrice;
|
|
246
|
-
const liq = await checkArbLiquidity(longAdapter, shortAdapter,
|
|
217
|
+
const liq = await checkArbLiquidity(longAdapter, shortAdapter, longSym, requestedUsd, 0.5, log, shortSym);
|
|
247
218
|
if (!liq.viable)
|
|
248
219
|
continue;
|
|
249
220
|
// Compute matched size (same for both legs) at each venue's published size precision —
|
|
@@ -251,7 +222,7 @@ export async function runFundingArb(adapters, params, jobId, log = console.log)
|
|
|
251
222
|
let longDec;
|
|
252
223
|
let shortDec;
|
|
253
224
|
try {
|
|
254
|
-
[longDec, shortDec] = await Promise.all([sizeDecimalsOf(longAdapter,
|
|
225
|
+
[longDec, shortDec] = await Promise.all([sizeDecimalsOf(longAdapter, longSym), sizeDecimalsOf(shortAdapter, shortSym)]);
|
|
255
226
|
}
|
|
256
227
|
catch (e) {
|
|
257
228
|
log(`[ARB] Skip ${symbol}: could not read the market size precision (${e instanceof Error ? e.message : String(e)})`);
|
|
@@ -272,7 +243,7 @@ export async function runFundingArb(adapters, params, jobId, log = console.log)
|
|
|
272
243
|
let longLev;
|
|
273
244
|
let shortLev;
|
|
274
245
|
try {
|
|
275
|
-
[longLev, shortLev] = await Promise.all([marketLeverageOf(longAdapter,
|
|
246
|
+
[longLev, shortLev] = await Promise.all([marketLeverageOf(longAdapter, longSym), marketLeverageOf(shortAdapter, shortSym)]);
|
|
276
247
|
}
|
|
277
248
|
catch (e) {
|
|
278
249
|
log(`[ARB] Skip ${symbol}: could not read the market leverage (${e instanceof Error ? e.message : String(e)})`);
|
|
@@ -288,8 +259,8 @@ export async function runFundingArb(adapters, params, jobId, log = console.log)
|
|
|
288
259
|
log(`[ARB] Opening: ${matched.size} ${symbol} — long ${longEx} then short ${shortEx}, each verified ($${matched.notional.toFixed(0)}/leg, slippage ~${liq.longSlippage.toFixed(2)}%/${liq.shortSlippage.toFixed(2)}%)...`);
|
|
289
260
|
const openPair = (size) => openPerpPairVerified({
|
|
290
261
|
source: SOURCE, sym: symbol,
|
|
291
|
-
longAdapter, longExch: longEx, longSym
|
|
292
|
-
shortAdapter, shortExch: shortEx, shortSym
|
|
262
|
+
longAdapter, longExch: longEx, longSym,
|
|
263
|
+
shortAdapter, shortExch: shortEx, shortSym,
|
|
293
264
|
size, smart: false, log, shortLotRetry: true,
|
|
294
265
|
});
|
|
295
266
|
let res = await openPair(matched.size);
|
|
@@ -323,6 +294,8 @@ export async function runFundingArb(adapters, params, jobId, log = console.log)
|
|
|
323
294
|
entrySpread: annualSpread,
|
|
324
295
|
openedAt: Date.now(),
|
|
325
296
|
entryPrices: { long: lowest.markPrice, short: highest.markPrice },
|
|
297
|
+
longSymbol: longSym,
|
|
298
|
+
shortSymbol: shortSym,
|
|
326
299
|
});
|
|
327
300
|
// Entry record: `arb status` / `arb close` confirm this pair from it (arbPairProvenance).
|
|
328
301
|
logExecution({
|
|
@@ -403,8 +376,8 @@ async function closeArbPosition(pos, adapters, log) {
|
|
|
403
376
|
let shortSize;
|
|
404
377
|
try {
|
|
405
378
|
[longSize, shortSize] = await Promise.all([
|
|
406
|
-
venueLegSize(longAdapter, pos.
|
|
407
|
-
venueLegSize(shortAdapter, pos.
|
|
379
|
+
venueLegSize(longAdapter, pos.longSymbol, "long").then(cap),
|
|
380
|
+
venueLegSize(shortAdapter, pos.shortSymbol, "short").then(cap),
|
|
408
381
|
]);
|
|
409
382
|
}
|
|
410
383
|
catch (err) {
|
|
@@ -418,8 +391,8 @@ async function closeArbPosition(pos, adapters, log) {
|
|
|
418
391
|
if (longSize !== null && shortSize !== null) {
|
|
419
392
|
const res = await closePerpPairVerified({
|
|
420
393
|
source: SOURCE, sym: pos.symbol,
|
|
421
|
-
longAdapter, longExch: pos.longExchange, longSym: pos.
|
|
422
|
-
shortAdapter, shortExch: pos.shortExchange, shortSym: pos.
|
|
394
|
+
longAdapter, longExch: pos.longExchange, longSym: pos.longSymbol, longSize,
|
|
395
|
+
shortAdapter, shortExch: pos.shortExchange, shortSym: pos.shortSymbol, shortSize,
|
|
423
396
|
smart: false, log,
|
|
424
397
|
});
|
|
425
398
|
if (res.status === "closed") {
|
|
@@ -448,7 +421,7 @@ async function closeArbPosition(pos, adapters, log) {
|
|
|
448
421
|
return false;
|
|
449
422
|
};
|
|
450
423
|
try {
|
|
451
|
-
const c = await closePerpVerified(adapter, venue, pos.
|
|
424
|
+
const c = await closePerpVerified(adapter, venue, leg === "long" ? pos.longSymbol : pos.shortSymbol, side, size);
|
|
452
425
|
if (c.residual > 0)
|
|
453
426
|
return q(`orphaned ${leg} close on ${venue} closed ${c.closed} of ${size}, ${c.residual} still open${c.note !== null ? ` (${c.note})` : ""}`);
|
|
454
427
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -34,7 +34,17 @@ export interface ApiResponse<T = unknown> {
|
|
|
34
34
|
skippedRows?: {
|
|
35
35
|
count: number;
|
|
36
36
|
reason: string;
|
|
37
|
-
};
|
|
37
|
+
}; /** Why a value is null / degraded (e.g. a venue's funding rate unavailable). */
|
|
38
|
+
warnings?: string[]; /** `arb scan --rates`: the --min threshold (annualized %) and how many pairs it hid. */
|
|
39
|
+
minSpread?: number;
|
|
40
|
+
belowMinSpread?: number; /** Public-address reads (`account positions --address`): the address read, "public", the sub-reads that failed and notes on an empty answer. */
|
|
41
|
+
address?: string;
|
|
42
|
+
source?: "public";
|
|
43
|
+
errors?: Record<string, {
|
|
44
|
+
code: string;
|
|
45
|
+
message: string;
|
|
46
|
+
}>;
|
|
47
|
+
notes?: string[];
|
|
38
48
|
};
|
|
39
49
|
}
|
|
40
50
|
/** Wrap successful result in standard envelope */
|
package/dist/venue-health.js
CHANGED
|
@@ -68,7 +68,7 @@ export async function probeVenueHealth(network, venueIds) {
|
|
|
68
68
|
const ping = pingFor(v.id, network);
|
|
69
69
|
return ping !== null
|
|
70
70
|
? ping()
|
|
71
|
-
: timed("markets", async () => { await v.publicData.fetchMarkets(network); return { ok: true, status: null }; });
|
|
71
|
+
: timed("markets", async () => { await v.publicData.fetchMarkets(network, { funding: false }); return { ok: true, status: null }; });
|
|
72
72
|
}));
|
|
73
73
|
const probes = probed.map((v, i) => ({ venue: v.id, ...results[i], apiStatus: v.apiStatus.kind }));
|
|
74
74
|
return { probes, skipped, allHealthy: probes.every(p => p.ok) };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arcus GET /v1/markets eligibility — the one rule for every Arcus surface (WS directory, CLI
|
|
3
|
+
* adapter market-data reads, public market data / funding / arb, MCP): a market is listed only
|
|
4
|
+
* while `status` is "ONLINE". OFFLINE markets (live 2026-10-19: F, BAC, CCL, VT, SGOV, RVI-USD —
|
|
5
|
+
* 6 of 70) publish placeholder zeros for mark / index / rate / OI / volume, never real values.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isArcusOnlineMarket(row: {
|
|
8
|
+
status?: unknown;
|
|
9
|
+
}): boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arcus GET /v1/markets eligibility — the one rule for every Arcus surface (WS directory, CLI
|
|
3
|
+
* adapter market-data reads, public market data / funding / arb, MCP): a market is listed only
|
|
4
|
+
* while `status` is "ONLINE". OFFLINE markets (live 2026-10-19: F, BAC, CCL, VT, SGOV, RVI-USD —
|
|
5
|
+
* 6 of 70) publish placeholder zeros for mark / index / rate / OI / volume, never real values.
|
|
6
|
+
*/
|
|
7
|
+
export function isArcusOnlineMarket(row) {
|
|
8
|
+
return row.status === "ONLINE";
|
|
9
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aster GET /fapi/v3/exchangeInfo market eligibility — the one rule for every Aster surface (WS
|
|
3
|
+
* directory, CLI adapter, public market data / funding / arb, MCP): a market is listed only while
|
|
4
|
+
* `contractType` is "PERPETUAL" and `status` is "TRADING". Live 2026-10-19: TRADING/PERPETUAL 605,
|
|
5
|
+
* SETTLING/PERPETUAL 23 (being delisted: TONUSDT, IRUSDT, …), PENDING_TRADING with contractType ""
|
|
6
|
+
* 6 (MBLUSDT, AFEEUSDT, …) — neither of the latter two streams or trades.
|
|
7
|
+
*
|
|
8
|
+
* The quote asset is NOT part of eligibility: USD1 / U quoted perps are live markets, listed by
|
|
9
|
+
* every surface (asterCliSymbol gives the CLI symbol, asterMatchable whether it gets a canonical).
|
|
10
|
+
*/
|
|
11
|
+
export declare function isAsterListedPerpetual(row: {
|
|
12
|
+
contractType?: unknown;
|
|
13
|
+
status?: unknown;
|
|
14
|
+
}): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* CLI symbol of a listed Aster perpetual: the base for the USDT-quoted market (ETHUSDT → ETH, the
|
|
17
|
+
* venue's main listing), the venue symbol for any other quote (BTCUSD1, BTCU) — unique per market
|
|
18
|
+
* and passed to the venue as is.
|
|
19
|
+
*/
|
|
20
|
+
export declare function asterCliSymbol(row: {
|
|
21
|
+
symbol: string;
|
|
22
|
+
baseAsset: string;
|
|
23
|
+
quoteAsset?: unknown;
|
|
24
|
+
}): string;
|
|
25
|
+
/**
|
|
26
|
+
* Whether a listed Aster market may carry a cross-venue canonical (then `base`): only its USDT
|
|
27
|
+
* market. USD1 / U quoted ones (BTCUSD1, GPROUSD1, BTCU) are listed but never matched across
|
|
28
|
+
* venues — so `arb` (scan / auto / exec) never takes one as a leg: Aster arb sizing, margin checks
|
|
29
|
+
* and transfers are USDT-collateral, and a USD1 / U margined leg is not handled.
|
|
30
|
+
*/
|
|
31
|
+
export declare function asterMatchable(row: {
|
|
32
|
+
quoteAsset?: unknown;
|
|
33
|
+
}): boolean;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aster GET /fapi/v3/exchangeInfo market eligibility — the one rule for every Aster surface (WS
|
|
3
|
+
* directory, CLI adapter, public market data / funding / arb, MCP): a market is listed only while
|
|
4
|
+
* `contractType` is "PERPETUAL" and `status` is "TRADING". Live 2026-10-19: TRADING/PERPETUAL 605,
|
|
5
|
+
* SETTLING/PERPETUAL 23 (being delisted: TONUSDT, IRUSDT, …), PENDING_TRADING with contractType ""
|
|
6
|
+
* 6 (MBLUSDT, AFEEUSDT, …) — neither of the latter two streams or trades.
|
|
7
|
+
*
|
|
8
|
+
* The quote asset is NOT part of eligibility: USD1 / U quoted perps are live markets, listed by
|
|
9
|
+
* every surface (asterCliSymbol gives the CLI symbol, asterMatchable whether it gets a canonical).
|
|
10
|
+
*/
|
|
11
|
+
export function isAsterListedPerpetual(row) {
|
|
12
|
+
return row.contractType === "PERPETUAL" && row.status === "TRADING";
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* CLI symbol of a listed Aster perpetual: the base for the USDT-quoted market (ETHUSDT → ETH, the
|
|
16
|
+
* venue's main listing), the venue symbol for any other quote (BTCUSD1, BTCU) — unique per market
|
|
17
|
+
* and passed to the venue as is.
|
|
18
|
+
*/
|
|
19
|
+
export function asterCliSymbol(row) {
|
|
20
|
+
return row.quoteAsset === "USDT" ? row.baseAsset : row.symbol;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether a listed Aster market may carry a cross-venue canonical (then `base`): only its USDT
|
|
24
|
+
* market. USD1 / U quoted ones (BTCUSD1, GPROUSD1, BTCU) are listed but never matched across
|
|
25
|
+
* venues — so `arb` (scan / auto / exec) never takes one as a leg: Aster arb sizing, margin checks
|
|
26
|
+
* and transfers are USDT-collateral, and a USD1 / U margined leg is not handled.
|
|
27
|
+
*/
|
|
28
|
+
export function asterMatchable(row) {
|
|
29
|
+
return row.quoteAsset === "USDT";
|
|
30
|
+
}
|
package/dist/ws/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Every perp-cli venue id. "ondo" and "qfex" are included for type stability but have no perp-cli/ws
|
|
7
|
-
* codec
|
|
7
|
+
* codec (planned, not yet available: there is no `ondo` / `qfex` export to pass to createVenueStream).
|
|
8
8
|
*/
|
|
9
9
|
type VenueId = "pacifica" | "hyperliquid" | "lighter" | "aster" | "lighter-rh" | "ondo" | "arcus" | "qfex";
|
|
10
10
|
declare const VENUE_IDS: readonly VenueId[];
|
|
@@ -74,6 +74,10 @@ declare class WsError extends Error {
|
|
|
74
74
|
remediation?: string;
|
|
75
75
|
details?: Record<string, unknown>;
|
|
76
76
|
});
|
|
77
|
+
/** `structured.code` — the same place as `code` on error events (`e.code`). */
|
|
78
|
+
get code(): WsErrorCode;
|
|
79
|
+
/** `structured.remediation` — the same place as `remediation` on error events; null when none. */
|
|
80
|
+
get remediation(): string | null;
|
|
77
81
|
}
|
|
78
82
|
/** Structural check: a PerpError (CLI) or a WsError — anything carrying `structured.{code,message}`. */
|
|
79
83
|
declare function isPerpError(e: unknown): e is {
|
|
@@ -109,6 +113,11 @@ interface WsLike {
|
|
|
109
113
|
wasClean?: boolean;
|
|
110
114
|
}) => void) | null;
|
|
111
115
|
onerror: ((ev: unknown) => void) | null;
|
|
116
|
+
/**
|
|
117
|
+
* Optional: settles once the underlying socket is fully closed (no timer or socket left). The
|
|
118
|
+
* Node transport sets it; `stream.close()` awaits it (bounded) so the process is idle afterwards.
|
|
119
|
+
*/
|
|
120
|
+
readonly whenClosed?: Promise<void>;
|
|
112
121
|
}
|
|
113
122
|
type WsFactory = (url: string) => WsLike;
|
|
114
123
|
type FetchLike = (input: string, init?: {
|
|
@@ -384,7 +393,7 @@ type UserRef = {
|
|
|
384
393
|
venue: "lighter" | "lighter-rh";
|
|
385
394
|
accountIndex: number;
|
|
386
395
|
}
|
|
387
|
-
/** KNOWN GAP
|
|
396
|
+
/** KNOWN GAP: Aster user channels are not implemented (CAPABILITY_UNSUPPORTED; planned, not yet available). */
|
|
388
397
|
| {
|
|
389
398
|
venue: "aster";
|
|
390
399
|
user: `0x${string}`;
|
|
@@ -527,7 +536,7 @@ interface VenueStreamOptions {
|
|
|
527
536
|
canonicalize?: Canonicalize;
|
|
528
537
|
/** Needed for kind:"user". */
|
|
529
538
|
credentials?: CredentialProvider;
|
|
530
|
-
/** Default false. Required for user channels whose auth needs a signing secret in the page (Aster/Ondo/QFEX,
|
|
539
|
+
/** Default false. Required for user channels whose auth needs a signing secret in the page (Aster / Ondo / QFEX — planned, not yet available). */
|
|
531
540
|
allowInPageSecrets?: boolean;
|
|
532
541
|
/** ≤ venue limits. */
|
|
533
542
|
budget?: {
|
|
@@ -807,8 +816,8 @@ declare function createVenueStream(codec: VenueCodec, opts?: VenueStreamOptions)
|
|
|
807
816
|
*/
|
|
808
817
|
declare const USD_LIKE_QUOTES: ReadonlySet<string>;
|
|
809
818
|
/**
|
|
810
|
-
* HIP-3 (dex set) → "<dex>:<BASE>".
|
|
811
|
-
* quote (null), → null (not comparable by default).
|
|
819
|
+
* HIP-3 (dex set) → "<dex>:<BASE>". An ambiguous ticker (isUnclassifiedTicker) → null. A USD-like
|
|
820
|
+
* quote → "<BASE>". Any other quote, or an unknown quote (null), → null (not comparable by default).
|
|
812
821
|
*/
|
|
813
822
|
declare const defaultCanonicalize: Canonicalize;
|
|
814
823
|
|
|
@@ -871,9 +880,17 @@ interface FundingComparator {
|
|
|
871
880
|
subscribe(cb: (rows: FundingRow[], changed: string[]) => void): () => void;
|
|
872
881
|
close(): void;
|
|
873
882
|
}
|
|
874
|
-
/**
|
|
883
|
+
/**
|
|
884
|
+
* Rate per hour as a FRACTION (0.0000125 = 0.00125 %/h): `rate / rateIntervalHours`.
|
|
885
|
+
* @experimental 0.15.x (stable from 0.16.0 after consumer sign-off)
|
|
886
|
+
*/
|
|
875
887
|
declare function toHourly(rate: Dec | null, rateIntervalHours: number | null): number | null;
|
|
876
|
-
/**
|
|
888
|
+
/**
|
|
889
|
+
* Simple annualized rate as a FRACTION, not a percent: `toHourly × 24 × 365` (0.0000125/h → 0.1095,
|
|
890
|
+
* i.e. 10.95 %). The CLI's `annualizedPct` (`arb scan --rates`, MCP `get_funding_rates`) is the same
|
|
891
|
+
* quantity in PERCENT (10.95): multiply `toApr` by 100 to compare the two.
|
|
892
|
+
* @experimental 0.15.x (stable from 0.16.0 after consumer sign-off)
|
|
893
|
+
*/
|
|
877
894
|
declare function toApr(rate: Dec | null, rateIntervalHours: number | null): number | null;
|
|
878
895
|
/** @experimental 0.15.x (stable from 0.16.0 after consumer sign-off) */
|
|
879
896
|
declare function createFundingComparator(opts: FundingComparatorOptions): FundingComparator;
|
|
@@ -916,7 +933,11 @@ declare const pacifica: VenueCodec;
|
|
|
916
933
|
*
|
|
917
934
|
* Units (Phase 0, the key correction): WS `funding_rate` (last settled) and `current_funding_rate`
|
|
918
935
|
* (predicted) are PERCENT per 1 h → converted to a fraction by an exact 2-place decimal shift,
|
|
919
|
-
* rateIntervalHours = settlementIntervalHours = 1. `open_interest` is USD notional (not base)
|
|
936
|
+
* rateIntervalHours = settlementIntervalHours = 1. `open_interest` is USD notional (not base) and
|
|
937
|
+
* one side — INFERRED, not documented by the venue: WS open_interest / mark = REST orderBookDetails
|
|
938
|
+
* `open_interest` (base) (2026-10-19, BTC: 174.88 M USD / 82,387 = 2,122.7 BTC), and the venue's
|
|
939
|
+
* exchangeMetrics open_interest — a DAILY snapshot, two days old when compared — was ≈ 2× the
|
|
940
|
+
* current one-side figure (336.6 M USD).
|
|
920
941
|
* Errors arrive as {error:{code,message}} with no channel field (matched by the channel name in
|
|
921
942
|
* the message). Spot markets (market_type "spot" in /api/v1/orderBooks) are not perps and skipped.
|
|
922
943
|
*
|
|
@@ -928,7 +949,7 @@ declare const lighter: VenueCodec;
|
|
|
928
949
|
declare const lighterRh: VenueCodec;
|
|
929
950
|
|
|
930
951
|
/**
|
|
931
|
-
* Aster codec (§6.1, public only
|
|
952
|
+
* Aster codec (§6.1, public channels only). One combined-stream socket (`/stream`, frames
|
|
932
953
|
* {stream, data}) with `!markPrice@arr@1s` (every symbol, 1 s) + `!ticker@arr` (only changed symbols).
|
|
933
954
|
* r = accruing current-period rate (Phase 0: == premiumIndex.lastFundingRate ≠ last settled).
|
|
934
955
|
* The funding interval is per market from the LABELLED REST source `rest:/fapi/v3/fundingInfo`
|