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.
Files changed (154) hide show
  1. package/README.ja.md +243 -0
  2. package/README.ko.md +243 -0
  3. package/README.md +129 -535
  4. package/README.zh.md +243 -0
  5. package/dist/api/public/lighter.d.ts +5 -0
  6. package/dist/api/public/lighter.js +5 -0
  7. package/dist/api/public/pacifica.js +2 -2
  8. package/dist/arb/sizing.js +4 -2
  9. package/dist/arb/state.d.ts +6 -0
  10. package/dist/balance-by-address.d.ts +39 -0
  11. package/dist/balance-by-address.js +44 -0
  12. package/dist/bot/quote-rules.js +1 -1
  13. package/dist/bot/strategies/funding-arb-strategy.js +10 -3
  14. package/dist/bot/strategies/funding-arb-utils.d.ts +14 -2
  15. package/dist/bot/strategies/funding-arb-utils.js +48 -15
  16. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
  17. package/dist/bot/strategies/spot-perp-arb-strategy.js +4 -2
  18. package/dist/bot/strategies/spot-perp-executor.js +2 -2
  19. package/dist/cli-program.js +1 -1
  20. package/dist/commands/account.js +168 -24
  21. package/dist/commands/arb/index.js +19 -77
  22. package/dist/commands/arb-auto.d.ts +24 -0
  23. package/dist/commands/arb-auto.js +155 -81
  24. package/dist/commands/arb.d.ts +8 -0
  25. package/dist/commands/arb.js +49 -23
  26. package/dist/commands/backtest.js +23 -21
  27. package/dist/commands/market.d.ts +48 -0
  28. package/dist/commands/market.js +241 -78
  29. package/dist/commands/options.d.ts +5 -0
  30. package/dist/commands/options.js +20 -3
  31. package/dist/commands/pacifica-earn.js +8 -21
  32. package/dist/commands/portfolio.d.ts +16 -0
  33. package/dist/commands/portfolio.js +51 -8
  34. package/dist/commands/trade.js +4 -4
  35. package/dist/cross-chain-margin.d.ts +3 -1
  36. package/dist/cross-chain-margin.js +4 -2
  37. package/dist/dashboard/server.js +2 -2
  38. package/dist/dashboard/ui.js +9 -6
  39. package/dist/errors.d.ts +5 -0
  40. package/dist/errors.js +10 -0
  41. package/dist/exchanges/address.d.ts +8 -0
  42. package/dist/exchanges/address.js +47 -0
  43. package/dist/exchanges/arcus/adapter.d.ts +10 -6
  44. package/dist/exchanges/arcus/adapter.js +33 -6
  45. package/dist/exchanges/arcus/public.js +3 -2
  46. package/dist/exchanges/aster.d.ts +27 -7
  47. package/dist/exchanges/aster.js +124 -42
  48. package/dist/exchanges/canonical-symbols.d.ts +7 -0
  49. package/dist/exchanges/canonical-symbols.js +20 -0
  50. package/dist/exchanges/capabilities.d.ts +2 -0
  51. package/dist/exchanges/capabilities.js +2 -1
  52. package/dist/exchanges/derive/options.d.ts +5 -0
  53. package/dist/exchanges/derive/options.js +20 -1
  54. package/dist/exchanges/extended/adapter.d.ts +16 -2
  55. package/dist/exchanges/extended/adapter.js +96 -34
  56. package/dist/exchanges/extended/public.d.ts +3 -0
  57. package/dist/exchanges/extended/public.js +9 -2
  58. package/dist/exchanges/grvt/adapter.d.ts +16 -2
  59. package/dist/exchanges/grvt/adapter.js +98 -21
  60. package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
  61. package/dist/exchanges/hyperliquid-outcome.js +29 -1
  62. package/dist/exchanges/hyperliquid-spot.d.ts +8 -0
  63. package/dist/exchanges/hyperliquid-spot.js +20 -12
  64. package/dist/exchanges/hyperliquid.d.ts +180 -13
  65. package/dist/exchanges/hyperliquid.js +487 -185
  66. package/dist/exchanges/interface.d.ts +64 -8
  67. package/dist/exchanges/interface.js +31 -0
  68. package/dist/exchanges/lighter-public.d.ts +66 -11
  69. package/dist/exchanges/lighter-public.js +249 -81
  70. package/dist/exchanges/lighter-spot.d.ts +7 -1
  71. package/dist/exchanges/lighter-spot.js +99 -90
  72. package/dist/exchanges/lighter.d.ts +70 -11
  73. package/dist/exchanges/lighter.js +285 -92
  74. package/dist/exchanges/ondo/adapter.d.ts +34 -8
  75. package/dist/exchanges/ondo/adapter.js +73 -16
  76. package/dist/exchanges/ondo/public.d.ts +27 -2
  77. package/dist/exchanges/ondo/public.js +63 -31
  78. package/dist/exchanges/outcome-interface.d.ts +4 -0
  79. package/dist/exchanges/pacifica-spot.d.ts +6 -0
  80. package/dist/exchanges/pacifica-spot.js +23 -15
  81. package/dist/exchanges/pacifica-vaults.d.ts +9 -0
  82. package/dist/exchanges/pacifica-vaults.js +14 -1
  83. package/dist/exchanges/pacifica.d.ts +77 -6
  84. package/dist/exchanges/pacifica.js +321 -130
  85. package/dist/exchanges/public-balances.d.ts +25 -0
  86. package/dist/exchanges/public-balances.js +158 -0
  87. package/dist/exchanges/public-positions.d.ts +43 -0
  88. package/dist/exchanges/public-positions.js +154 -0
  89. package/dist/exchanges/qfex/adapter.d.ts +52 -9
  90. package/dist/exchanges/qfex/adapter.js +74 -9
  91. package/dist/exchanges/qfex/public.d.ts +62 -9
  92. package/dist/exchanges/qfex/public.js +128 -22
  93. package/dist/exchanges/qfex/ws-transport.d.ts +1 -1
  94. package/dist/exchanges/risex/adapter.d.ts +19 -7
  95. package/dist/exchanges/risex/adapter.js +58 -12
  96. package/dist/exchanges/spot-factory.js +1 -1
  97. package/dist/exchanges/txflow/adapter.d.ts +14 -7
  98. package/dist/exchanges/txflow/adapter.js +51 -19
  99. package/dist/exchanges/txflow/client.js +3 -1
  100. package/dist/exchanges/txflow/public.d.ts +12 -1
  101. package/dist/exchanges/txflow/public.js +82 -11
  102. package/dist/exchanges/unknown-symbol.d.ts +6 -1
  103. package/dist/exchanges/unknown-symbol.js +7 -2
  104. package/dist/exchanges/variational/adapter.d.ts +2 -6
  105. package/dist/exchanges/venue-types.d.ts +46 -3
  106. package/dist/exchanges/venue-types.js +80 -9
  107. package/dist/exchanges/venue.d.ts +99 -3
  108. package/dist/exchanges/venue.js +72 -0
  109. package/dist/exchanges/venues/hyperliquid.js +9 -0
  110. package/dist/exchanges/venues/lighter-rh.js +13 -3
  111. package/dist/exchanges/venues/lighter.js +13 -3
  112. package/dist/exchanges/venues/pacifica.js +9 -0
  113. package/dist/exchanges/venues/risex.js +15 -0
  114. package/dist/funding/legacy-public.d.ts +5 -3
  115. package/dist/funding/legacy-public.js +33 -37
  116. package/dist/funding/rates.d.ts +54 -5
  117. package/dist/funding/rates.js +49 -13
  118. package/dist/liquidity.d.ts +3 -1
  119. package/dist/liquidity.js +8 -4
  120. package/dist/mcp-pacifica-earn.js +11 -5
  121. package/dist/mcp-server.js +104 -15
  122. package/dist/pacifica/client.d.ts +2 -1
  123. package/dist/pacifica/client.js +11 -0
  124. package/dist/positions-by-address.d.ts +34 -0
  125. package/dist/positions-by-address.js +46 -0
  126. package/dist/strategies/funding-arb.js +35 -62
  127. package/dist/utils.d.ts +11 -1
  128. package/dist/venue-health.js +1 -1
  129. package/dist/ws/arcus-market.d.ts +9 -0
  130. package/dist/ws/arcus-market.js +9 -0
  131. package/dist/ws/aster-market.d.ts +33 -0
  132. package/dist/ws/aster-market.js +30 -0
  133. package/dist/ws/index.d.ts +30 -9
  134. package/dist/ws/index.js +180 -15
  135. package/dist/ws/lighter-market.d.ts +14 -0
  136. package/dist/ws/lighter-market.js +13 -0
  137. package/dist/ws/node.d.ts +30 -9
  138. package/dist/ws/node.js +204 -16
  139. package/dist/ws/unclassified.d.ts +16 -0
  140. package/dist/ws/unclassified.js +43 -0
  141. package/package.json +15 -2
  142. package/skills/perp-cli/SKILL.md +97 -33
  143. package/skills/perp-cli/package.json +15 -2
  144. package/skills/perp-cli/references/agent-operations.md +15 -12
  145. package/skills/perp-cli/references/commands.md +67 -12
  146. package/skills/perp-cli/references/strategies.md +38 -3
  147. package/skills/perp-cli/references/ws-library.md +55 -24
  148. package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
  149. package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
  150. package/skills/perp-cli/scripts/preflight.sh +27 -19
  151. package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
  152. package/skills/perp-cli/scripts/validate-arb.sh +56 -39
  153. package/dist/cli-spec.d.ts +0 -46
  154. 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, logSettledRejections } from "../utils.js";
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 { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates, } from "../shared-api.js";
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
- // SSOT rule #2: each DEX is independent. allSettled keeps the comparison
25
- // alive when one DEX errors out, while rejection reasons get logged to
26
- // stderr (explicit failure, never silent).
27
- const [pacRates, hlRates, ltRates] = await Promise.allSettled([
28
- fetchPacificaPrices().then(assets => assets.map(p => ({
29
- exchange: "pacifica", symbol: p.symbol, fundingRate: p.funding, markPrice: p.mark,
30
- }))),
31
- fetchHyperliquidMeta().then(assets => assets.map(a => ({
32
- exchange: "hyperliquid", symbol: a.symbol, fundingRate: a.funding, markPrice: a.markPx,
33
- }))),
34
- (async () => {
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 (params.symbols?.length && !params.symbols.includes(r.symbol))
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, symbol, requestedUsd, 0.5, log);
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, symbol), sizeDecimalsOf(shortAdapter, symbol)]);
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, symbol), marketLeverageOf(shortAdapter, symbol)]);
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: symbol,
292
- shortAdapter, shortExch: shortEx, shortSym: symbol,
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.symbol, "long").then(cap),
407
- venueLegSize(shortAdapter, pos.symbol, "short").then(cap),
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.symbol, longSize,
422
- shortAdapter, shortExch: pos.shortExchange, shortSym: pos.symbol, shortSize,
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.symbol, side, size);
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 */
@@ -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
+ }
@@ -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 until 0.15.1 (there is no `ondo` / `qfex` export to pass to createVenueStream yet).
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 in 0.15.0: Aster user channels are not implemented (CAPABILITY_UNSUPPORTED); 0.15.1. */
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, 0.15.1). */
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>". A USD-like quote → "<BASE>". Any other quote, or an unknown
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
- /** @experimental 0.15.x (stable from 0.16.0 after consumer sign-off) */
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
- /** @experimental 0.15.x (stable from 0.16.0 after consumer sign-off) */
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 in 0.15.0). One combined-stream socket (`/stream`, frames
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`