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
package/dist/ws/index.js CHANGED
@@ -37,6 +37,14 @@ var WsError = class extends Error {
37
37
  ...opts?.details !== void 0 ? { details: opts.details } : {}
38
38
  };
39
39
  }
40
+ /** `structured.code` — the same place as `code` on error events (`e.code`). */
41
+ get code() {
42
+ return this.structured.code;
43
+ }
44
+ /** `structured.remediation` — the same place as `remediation` on error events; null when none. */
45
+ get remediation() {
46
+ return this.structured.remediation === void 0 ? null : this.structured.remediation;
47
+ }
40
48
  };
41
49
  function isPerpError(e) {
42
50
  if (e === null || typeof e !== "object") return false;
@@ -45,11 +53,89 @@ function isPerpError(e) {
45
53
  return typeof s.code === "string" && typeof s.message === "string";
46
54
  }
47
55
 
56
+ // src/ws/unclassified.ts
57
+ var UNCLASSIFIED_TICKERS = {
58
+ hyperliquid: /* @__PURE__ */ new Set(["SPX"]),
59
+ aster: /* @__PURE__ */ new Set(["SPX"]),
60
+ pacifica: /* @__PURE__ */ new Set(["SPX", "BP", "CHIP"]),
61
+ lighter: /* @__PURE__ */ new Set(["BB", "BOT", "WEN", "QNT", "SPX"])
62
+ };
63
+ var LIGHTER_RH_CLASS = {
64
+ // crypto
65
+ BTC: "crypto",
66
+ ETH: "crypto",
67
+ SOL: "crypto",
68
+ HYPE: "crypto",
69
+ ZEC: "crypto",
70
+ LIT: "crypto",
71
+ VVV: "crypto",
72
+ SUI: "crypto",
73
+ NEAR: "crypto",
74
+ XRP: "crypto",
75
+ CASHCAT: "crypto",
76
+ PONS: "crypto",
77
+ // commodity (canonical via EXPLICIT_CANONICAL: XAU → GOLD, XAG → SILVER)
78
+ XAU: "commodity",
79
+ XAG: "commodity",
80
+ // etf
81
+ SPY: "etf",
82
+ QQQ: "etf",
83
+ SOXL: "etf",
84
+ SLV: "etf",
85
+ USO: "etf",
86
+ SGOV: "etf",
87
+ // equity
88
+ AAPL: "equity",
89
+ AMZN: "equity",
90
+ GOOGL: "equity",
91
+ META: "equity",
92
+ MSFT: "equity",
93
+ NVDA: "equity",
94
+ TSLA: "equity",
95
+ AMD: "equity",
96
+ INTC: "equity",
97
+ MU: "equity",
98
+ TSM: "equity",
99
+ ORCL: "equity",
100
+ PLTR: "equity",
101
+ COIN: "equity",
102
+ CRCL: "equity",
103
+ CRWV: "equity",
104
+ BABA: "equity",
105
+ SNDK: "equity",
106
+ SMCI: "equity",
107
+ SOFI: "equity",
108
+ AMC: "equity",
109
+ RGTI: "equity",
110
+ QBTS: "equity",
111
+ CLSK: "equity",
112
+ IREN: "equity",
113
+ WULF: "equity",
114
+ LUNR: "equity",
115
+ ASTS: "equity",
116
+ BE: "equity",
117
+ USAR: "equity",
118
+ SPCX: "equity",
119
+ SKHY: "equity",
120
+ // pre-IPO / private (explicit-only canonical → null unless mapped)
121
+ OPENAI: "preIpo",
122
+ ANTHROPIC: "preIpo",
123
+ SHEIN: "preIpo",
124
+ ANSEM: "preIpo",
125
+ AI: "preIpo"
126
+ };
127
+ function isUnclassifiedTicker(venue, base) {
128
+ const up = base.toUpperCase();
129
+ if (venue === "lighter-rh") return LIGHTER_RH_CLASS[up] === void 0;
130
+ return UNCLASSIFIED_TICKERS[venue]?.has(up) === true;
131
+ }
132
+
48
133
  // src/ws/canonical.ts
49
134
  var USD_LIKE_QUOTES = /* @__PURE__ */ new Set(["USDT", "USD1", "USDC", "USD", "USDG"]);
50
- var defaultCanonicalize = (_venue, m) => {
135
+ var defaultCanonicalize = (venue, m) => {
51
136
  const base = m.base.toUpperCase();
52
137
  if (m.dex !== null && m.dex !== "") return `${m.dex}:${base}`;
138
+ if (isUnclassifiedTicker(venue, base)) return null;
53
139
  if (m.quote === null) return null;
54
140
  return USD_LIKE_QUOTES.has(m.quote.toUpperCase()) ? base : null;
55
141
  };
@@ -122,6 +208,7 @@ var Connection = class {
122
208
  this.sendQueue = [];
123
209
  this.sendTimer = null;
124
210
  this.reconnectTimer = null;
211
+ this.closing = /* @__PURE__ */ new Set();
125
212
  this.staleTimer = null;
126
213
  this.pingTimer = null;
127
214
  this.awaitingPongSince = null;
@@ -446,15 +533,26 @@ var Connection = class {
446
533
  ws.close(code, reason);
447
534
  } catch {
448
535
  }
536
+ if (ws.whenClosed !== void 0) {
537
+ const p = ws.whenClosed;
538
+ this.closing.add(p);
539
+ void p.then(() => {
540
+ this.closing.delete(p);
541
+ });
542
+ }
449
543
  }
450
544
  }
545
+ /** Settles when every socket this connection closed is fully gone (transports that report it). */
546
+ drained() {
547
+ return Promise.all(Array.from(this.closing)).then(() => void 0);
548
+ }
451
549
  };
452
550
  function stateDetails(s) {
453
551
  const { status: _status, ...rest } = s;
454
552
  return rest;
455
553
  }
456
554
  function closedError(venue) {
457
- return new WsError("INVALID_PARAMS", `${venue} stream is closed`, { exchange: venue, remediation: "Create a new stream" });
555
+ return new WsError("INVALID_PARAMS", `${venue} stream is closed`, { exchange: venue, remediation: "Create a new stream", details: { closeReason: "CLIENT_CLOSE" } });
458
556
  }
459
557
 
460
558
  // src/ws/market-state.ts
@@ -972,6 +1070,7 @@ function checkCodec(codec) {
972
1070
  throw new WsError("INVALID_PARAMS", `createVenueStream: first argument must be a venue codec (${problem})`, { remediation: CODEC_REMEDIATION });
973
1071
  }
974
1072
  }
1073
+ var CLOSE_DRAIN_MS = 2e3;
975
1074
  function suggestCanonicals(missing, entries) {
976
1075
  const m = missing.toUpperCase();
977
1076
  const out = entries.filter((e) => e.canonical !== null && (e.canonical.toUpperCase() === m || e.base.toUpperCase() === m || e.canonical.toUpperCase().endsWith(`:${m}`))).map((e) => e.canonical);
@@ -979,7 +1078,7 @@ function suggestCanonicals(missing, entries) {
979
1078
  }
980
1079
  function suggestSymbols(missing, entries) {
981
1080
  const m = missing.toUpperCase();
982
- const out = entries.filter((e) => e.symbol.toUpperCase() === m || e.canonical !== null && e.canonical.toUpperCase() === m || e.base.toUpperCase() === m).map((e) => e.symbol);
1081
+ const out = entries.filter((e) => e.symbol.toUpperCase() === m || e.canonical !== null && (e.canonical.toUpperCase() === m || e.base.toUpperCase() === m)).map((e) => e.symbol);
983
1082
  return Array.from(new Set(out)).slice(0, 5);
984
1083
  }
985
1084
  var EVENT_CHANNEL = { account: "account", positions: "positions", orders: "orders", fill: "fills", fills: "fills" };
@@ -1738,7 +1837,7 @@ function createVenueStream(codec, opts = {}) {
1738
1837
  }
1739
1838
  if (target.kind === "market") {
1740
1839
  const channels = target.channels;
1741
- const valid = `Use channels ${MARKET_CHANNELS.map((c) => `"${c}"`).join(" / ")} (book and trades arrive in 0.15.1)`;
1840
+ const valid = `Use channels ${MARKET_CHANNELS.map((c) => `"${c}"`).join(" / ")} (book and trades: planned, not yet available)`;
1742
1841
  if (!Array.isArray(channels) || channels.length === 0) {
1743
1842
  throw wsError("INVALID_PARAMS", 'kind "market" needs at least one channel', valid);
1744
1843
  }
@@ -1749,7 +1848,7 @@ function createVenueStream(codec, opts = {}) {
1749
1848
  throw wsError(
1750
1849
  "CAPABILITY_UNSUPPORTED",
1751
1850
  `${venue} channel(s) ${later.join(", ")} are not available in this release`,
1752
- 'Use channels "market" / "bbo" (book and trades arrive in 0.15.1)'
1851
+ 'Use channels "market" / "bbo" (book and trades: planned, not yet available)'
1753
1852
  );
1754
1853
  }
1755
1854
  if (target.channels.includes("bbo") && !codec.capabilities.channels.bbo) {
@@ -2063,6 +2162,14 @@ function createVenueStream(codec, opts = {}) {
2063
2162
  sub.end();
2064
2163
  }
2065
2164
  stateListeners.clear();
2165
+ let bound = null;
2166
+ await Promise.race([
2167
+ Promise.all(shards.map((sh) => sh.conn.drained())),
2168
+ new Promise((resolve) => {
2169
+ bound = setTimeout(resolve, CLOSE_DRAIN_MS);
2170
+ })
2171
+ ]);
2172
+ if (bound !== null) clearTimeout(bound);
2066
2173
  }
2067
2174
  };
2068
2175
  return stream;
@@ -2424,6 +2531,23 @@ function mulDecimalInt(s, n, shift) {
2424
2531
  const abs2 = (neg ? -product : product).toString();
2425
2532
  return shiftDecimal(`${neg ? "-" : ""}${abs2}`, shift - frac.length);
2426
2533
  }
2534
+ function sumDecimals(values) {
2535
+ let scale = 0;
2536
+ const parts = [];
2537
+ for (const v of values) {
2538
+ const m = DECIMAL_RE.exec(v);
2539
+ if (m === null || m[4] !== void 0 || m[2] === "" && (m[3] === void 0 || m[3] === "")) return void 0;
2540
+ const frac = m[3] === void 0 ? "" : m[3];
2541
+ const digits = `${m[2]}${frac}`.replace(/^0+(?=\d)/, "");
2542
+ parts.push({ int: BigInt(digits === "" ? "0" : digits) * (m[1] === "-" ? -1n : 1n), scale: frac.length });
2543
+ if (frac.length > scale) scale = frac.length;
2544
+ }
2545
+ let total = 0n;
2546
+ for (const p of parts) total += p.int * 10n ** BigInt(scale - p.scale);
2547
+ const neg = total < 0n;
2548
+ const abs2 = (neg ? -total : total).toString();
2549
+ return shiftDecimal(`${neg ? "-" : ""}${abs2}`, -scale);
2550
+ }
2427
2551
 
2428
2552
  // src/ws/codecs/common.ts
2429
2553
  function isObj(v) {
@@ -2504,9 +2628,9 @@ function capabilities(p) {
2504
2628
  channels: { allMarkets: true, market: true, bbo: true, book: false, trades: false, user: p.user },
2505
2629
  fields: p.fields,
2506
2630
  notes: {
2507
- book: "Not in this release (0.15.1)",
2508
- trades: "Not in this release (0.15.1)",
2509
- ...p.user ? {} : { user: "Not in this release (0.15.1)" },
2631
+ book: "Planned, not yet available",
2632
+ trades: "Planned, not yet available",
2633
+ ...p.user ? {} : { user: "Planned, not yet available" },
2510
2634
  ...p.notes
2511
2635
  }
2512
2636
  };
@@ -2746,6 +2870,16 @@ var SIDE = { B: "buy", A: "sell" };
2746
2870
  function abs(d) {
2747
2871
  return d === null ? null : d.replace(/^-/, "");
2748
2872
  }
2873
+ function accountUnrealized(rows, bad) {
2874
+ if (bad.includes("assetPositions") || bad.includes("assetPositions[]")) return null;
2875
+ const values = [];
2876
+ for (const r of rows) {
2877
+ if (r.unrealizedPnl === null) return null;
2878
+ values.push(r.unrealizedPnl);
2879
+ }
2880
+ const sum = sumDecimals(values);
2881
+ return sum === void 0 ? null : sum;
2882
+ }
2749
2883
  function decodeClearinghouse(d) {
2750
2884
  if (!isObj(d) || typeof d.dex !== "string" || !isObj(d.clearinghouseState)) return [protocolError("hyperliquid clearinghouseState frame without dex/clearinghouseState")];
2751
2885
  const wire = `clearinghouseState:${d.dex}`;
@@ -2788,8 +2922,10 @@ function decodeClearinghouse(d) {
2788
2922
  equity: ms === null ? null : dec(ms, "accountValue", bad),
2789
2923
  available: dec(cs, "withdrawable", bad),
2790
2924
  marginUsed: ms === null ? null : dec(ms, "totalMarginUsed", bad),
2791
- unrealizedPnl: null
2792
- // not in clearinghouseState (per-position only)
2925
+ // clearinghouseState has no account-level unrealizedPnl, but the frame carries every
2926
+ // position of this dex with its own `unrealizedPnl`: the account value is their exact sum
2927
+ // ("0" with no position). null when any position's value is missing (unknown, never guessed).
2928
+ unrealizedPnl: accountUnrealized(rows, bad)
2793
2929
  },
2794
2930
  { kind: "positions", snapshot: true, scope: d.dex, rows, remove: [] }
2795
2931
  ];
@@ -3284,6 +3420,11 @@ function decodeBbo2(d) {
3284
3420
  return out;
3285
3421
  }
3286
3422
 
3423
+ // src/ws/lighter-market.ts
3424
+ function isLighterMarketLive(row) {
3425
+ return row.status === "active" && row.is_frozen !== true;
3426
+ }
3427
+
3287
3428
  // src/ws/codecs/lighter.ts
3288
3429
  var ALL2 = "market_stats/all";
3289
3430
  var ORDERS_TOKEN_TTL_SEC = 3600;
@@ -3292,6 +3433,10 @@ var ZERO = /^-?0*(\.0*)?$/;
3292
3433
  function bookSide(o, key, bad) {
3293
3434
  return o[key] === "" ? null : readDec(o, key, bad);
3294
3435
  }
3436
+ function lastTrade(o, bad) {
3437
+ const v = readDec(o, "last_trade_price", bad);
3438
+ return v !== void 0 && v !== null && ZERO.test(v) ? null : v;
3439
+ }
3295
3440
  function percentToFraction(o, key, bad) {
3296
3441
  const v = readDec(o, key, bad);
3297
3442
  if (v === void 0 || v === null) return v;
@@ -3324,7 +3469,7 @@ function makeLighter(venue, quote) {
3324
3469
  notes: {
3325
3470
  rate: "WS funding is PERCENT per 1 h on the wire; emitted as a fraction per 1 h",
3326
3471
  nextFundingTime: "Hourly settlement rule (venue-definition)",
3327
- openInterest: "USD notional (Phase 0), sides not stated by the venue",
3472
+ openInterest: "USD notional, one side \u2014 inferred, not documented by the venue (= REST orderBookDetails open_interest, base, \xD7 mark; the venue's exchangeMetrics kind=open_interest, a daily snapshot, read \u2248 2\xD7 when compared with the current value)",
3328
3473
  user: "By accountIndex. user_stats / account_all_positions / account_all_trades need no auth; account_all_orders needs CredentialProvider.authToken (re-called at 80 % TTL). account_all_trades does not replay \u2192 gap refetch-required for fills after a reconnect",
3329
3474
  clientId: "orders: client_order_id; fills: ask_/bid_client_id_str of our side; 0 = no client id \u2192 null",
3330
3475
  liquidationPrice: 'liquidation_price; "0" (no liquidation price, e.g. well-covered cross margin) \u2192 null, as in the CLI REST path',
@@ -3341,7 +3486,7 @@ function makeLighter(venue, quote) {
3341
3486
  limits: S5.limits,
3342
3487
  closesOnBadSubscribe: false,
3343
3488
  staleAfterMs: S5.staleAfterMs,
3344
- openInterest: { unit: "usd", sides: null },
3489
+ openInterest: { unit: "usd", sides: "one" },
3345
3490
  refresh: null,
3346
3491
  async loadDirectory(ctx) {
3347
3492
  const raw = await fetchJson(ctx.fetch, venue, `${ctx.restUrl}/api/v1/orderBooks`);
@@ -3359,6 +3504,10 @@ function makeLighter(venue, quote) {
3359
3504
  continue;
3360
3505
  }
3361
3506
  idToSymbol.set(id, m.symbol);
3507
+ if (!isLighterMarketLive(m)) {
3508
+ nonPerp.add(id);
3509
+ continue;
3510
+ }
3362
3511
  entries.push({
3363
3512
  venue,
3364
3513
  dex: null,
@@ -3494,7 +3643,7 @@ function decodeStats(venue, f, d, recvTs) {
3494
3643
  index: readDec(r, "index_price", bad),
3495
3644
  bestBid: bookSide(r, "best_bid_price", bad),
3496
3645
  bestAsk: bookSide(r, "best_ask_price", bad),
3497
- last: readDec(r, "last_trade_price", bad),
3646
+ last: lastTrade(r, bad),
3498
3647
  rate: percentToFraction(r, "funding_rate", bad),
3499
3648
  predictedRate: percentToFraction(r, "current_funding_rate", bad),
3500
3649
  openInterest: readDec(r, "open_interest", bad),
@@ -3710,6 +3859,14 @@ function decodeTicker(venue, wire, f, d) {
3710
3859
  return out;
3711
3860
  }
3712
3861
 
3862
+ // src/ws/aster-market.ts
3863
+ function isAsterListedPerpetual(row) {
3864
+ return row.contractType === "PERPETUAL" && row.status === "TRADING";
3865
+ }
3866
+ function asterMatchable(row) {
3867
+ return row.quoteAsset === "USDT";
3868
+ }
3869
+
3713
3870
  // src/ws/codecs/aster.ts
3714
3871
  var S3 = VENUE_STATIC.aster;
3715
3872
  var MARK = "!markPrice@arr@1s";
@@ -3757,7 +3914,7 @@ var aster = {
3757
3914
  openInterest: "Not on WS (REST only)",
3758
3915
  fundingInterval: `Labelled REST source ${FUNDING_INFO}, refreshed hourly; missing \u2192 null`,
3759
3916
  volume24hNotional: "!ticker@arr q (quote-asset volume)",
3760
- user: 'KNOWN GAP in 0.15.0: no Aster user channels (signed USER_STREAM listenKey) \u2014 kind:"user" rejects CAPABILITY_UNSUPPORTED; arrives in 0.15.1',
3917
+ user: 'KNOWN GAP: no Aster user channels (signed USER_STREAM listenKey) \u2014 kind:"user" rejects CAPABILITY_UNSUPPORTED; planned, not yet available',
3761
3918
  bbo: "<symbol>@bookTicker; `u` orders frames (non-contiguous)"
3762
3919
  }
3763
3920
  }),
@@ -3797,6 +3954,7 @@ var aster = {
3797
3954
  if (!isObj(s) || typeof s.symbol !== "string" || typeof s.baseAsset !== "string") {
3798
3955
  throw badShape("aster", "exchangeInfo symbol without symbol/baseAsset");
3799
3956
  }
3957
+ if (!isAsterListedPerpetual(s)) continue;
3800
3958
  const quote = typeof s.quoteAsset === "string" && s.quoteAsset !== "" ? s.quoteAsset : null;
3801
3959
  listed.add(s.symbol);
3802
3960
  const hours = intervals.get(s.symbol);
@@ -3807,7 +3965,8 @@ var aster = {
3807
3965
  venueSymbol: s.symbol,
3808
3966
  base: s.baseAsset,
3809
3967
  quote,
3810
- canonical: ctx.canonicalize("aster", { venueSymbol: s.symbol, dex: null, base: s.baseAsset, quote }),
3968
+ // Only the USDT listing is matchable (asterMatchable — the CLI rule): BTCUSD1 is never a second BTC leg.
3969
+ canonical: asterMatchable(s) ? ctx.canonicalize("aster", { venueSymbol: s.symbol, dex: null, base: s.baseAsset, quote }) : null,
3811
3970
  rateIntervalHours: hours === void 0 ? null : hours,
3812
3971
  settlementIntervalHours: hours === void 0 ? null : hours,
3813
3972
  source: "rest:/fapi/v3/exchangeInfo"
@@ -3930,6 +4089,11 @@ function decodeArr(wire, data2, d, fn) {
3930
4089
  return out;
3931
4090
  }
3932
4091
 
4092
+ // src/ws/arcus-market.ts
4093
+ function isArcusOnlineMarket(row) {
4094
+ return row.status === "ONLINE";
4095
+ }
4096
+
3933
4097
  // src/ws/codecs/arcus.ts
3934
4098
  var S4 = VENUE_STATIC.arcus;
3935
4099
  var MARKETS = "markets";
@@ -3980,6 +4144,7 @@ var arcus = {
3980
4144
  if (!isObj(m) || typeof m.marketDisplayName !== "string" || typeof m.marketId !== "number" || typeof m.baseAsset !== "string") {
3981
4145
  throw badShape("arcus", "market without marketDisplayName/marketId/baseAsset");
3982
4146
  }
4147
+ if (!isArcusOnlineMarket(m)) continue;
3983
4148
  const quote = typeof m.quoteAsset === "string" ? m.quoteAsset : null;
3984
4149
  idToSymbol.set(String(m.marketId), m.marketDisplayName);
3985
4150
  entries.push({
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Lighter / Lighter RH market eligibility — the ONE rule for every Lighter surface (WS directory,
3
+ * CLI adapter `market list|prices|info`, public data `tradable`): a market is live when its
4
+ * `/api/v1/orderBooks` / `/api/v1/orderBookDetails` row has `status: "active"` (OpenAPI enum
5
+ * "active" | "inactive") and is not `is_frozen`.
6
+ *
7
+ * Live 2026-10-19 (mainnet): orderBookDetails lists 239 perps (213 active, 26 inactive) and
8
+ * orderBooks 235 (213 active, 22 inactive). The four perps only in orderBookDetails (MYX, RESOLV,
9
+ * MKR, DOLO) are all inactive, so both endpoints give the same 213 live perps under this rule.
10
+ */
11
+ export declare function isLighterMarketLive(row: {
12
+ status?: unknown;
13
+ is_frozen?: unknown;
14
+ }): boolean;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Lighter / Lighter RH market eligibility — the ONE rule for every Lighter surface (WS directory,
3
+ * CLI adapter `market list|prices|info`, public data `tradable`): a market is live when its
4
+ * `/api/v1/orderBooks` / `/api/v1/orderBookDetails` row has `status: "active"` (OpenAPI enum
5
+ * "active" | "inactive") and is not `is_frozen`.
6
+ *
7
+ * Live 2026-10-19 (mainnet): orderBookDetails lists 239 perps (213 active, 26 inactive) and
8
+ * orderBooks 235 (213 active, 22 inactive). The four perps only in orderBookDetails (MYX, RESOLV,
9
+ * MKR, DOLO) are all inactive, so both endpoints give the same 213 live perps under this rule.
10
+ */
11
+ export function isLighterMarketLive(row) {
12
+ return row.status === "active" && row.is_frozen !== true;
13
+ }
package/dist/ws/node.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 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?: {
@@ -792,8 +801,8 @@ interface VenueCodec {
792
801
  */
793
802
  declare const USD_LIKE_QUOTES: ReadonlySet<string>;
794
803
  /**
795
- * HIP-3 (dex set) → "<dex>:<BASE>". A USD-like quote → "<BASE>". Any other quote, or an unknown
796
- * quote (null), → null (not comparable by default).
804
+ * HIP-3 (dex set) → "<dex>:<BASE>". An ambiguous ticker (isUnclassifiedTicker) → null. A USD-like
805
+ * quote → "<BASE>". Any other quote, or an unknown quote (null), → null (not comparable by default).
797
806
  */
798
807
  declare const defaultCanonicalize: Canonicalize;
799
808
 
@@ -856,9 +865,17 @@ interface FundingComparator {
856
865
  subscribe(cb: (rows: FundingRow[], changed: string[]) => void): () => void;
857
866
  close(): void;
858
867
  }
859
- /** @experimental 0.15.x (stable from 0.16.0 after consumer sign-off) */
868
+ /**
869
+ * Rate per hour as a FRACTION (0.0000125 = 0.00125 %/h): `rate / rateIntervalHours`.
870
+ * @experimental 0.15.x (stable from 0.16.0 after consumer sign-off)
871
+ */
860
872
  declare function toHourly(rate: Dec | null, rateIntervalHours: number | null): number | null;
861
- /** @experimental 0.15.x (stable from 0.16.0 after consumer sign-off) */
873
+ /**
874
+ * Simple annualized rate as a FRACTION, not a percent: `toHourly × 24 × 365` (0.0000125/h → 0.1095,
875
+ * i.e. 10.95 %). The CLI's `annualizedPct` (`arb scan --rates`, MCP `get_funding_rates`) is the same
876
+ * quantity in PERCENT (10.95): multiply `toApr` by 100 to compare the two.
877
+ * @experimental 0.15.x (stable from 0.16.0 after consumer sign-off)
878
+ */
862
879
  declare function toApr(rate: Dec | null, rateIntervalHours: number | null): number | null;
863
880
  /** @experimental 0.15.x (stable from 0.16.0 after consumer sign-off) */
864
881
  declare function createFundingComparator(opts: FundingComparatorOptions): FundingComparator;
@@ -901,7 +918,11 @@ declare const pacifica: VenueCodec;
901
918
  *
902
919
  * Units (Phase 0, the key correction): WS `funding_rate` (last settled) and `current_funding_rate`
903
920
  * (predicted) are PERCENT per 1 h → converted to a fraction by an exact 2-place decimal shift,
904
- * rateIntervalHours = settlementIntervalHours = 1. `open_interest` is USD notional (not base).
921
+ * rateIntervalHours = settlementIntervalHours = 1. `open_interest` is USD notional (not base) and
922
+ * one side — INFERRED, not documented by the venue: WS open_interest / mark = REST orderBookDetails
923
+ * `open_interest` (base) (2026-10-19, BTC: 174.88 M USD / 82,387 = 2,122.7 BTC), and the venue's
924
+ * exchangeMetrics open_interest — a DAILY snapshot, two days old when compared — was ≈ 2× the
925
+ * current one-side figure (336.6 M USD).
905
926
  * Errors arrive as {error:{code,message}} with no channel field (matched by the channel name in
906
927
  * the message). Spot markets (market_type "spot" in /api/v1/orderBooks) are not perps and skipped.
907
928
  *
@@ -913,7 +934,7 @@ declare const lighter: VenueCodec;
913
934
  declare const lighterRh: VenueCodec;
914
935
 
915
936
  /**
916
- * Aster codec (§6.1, public only in 0.15.0). One combined-stream socket (`/stream`, frames
937
+ * Aster codec (§6.1, public channels only). One combined-stream socket (`/stream`, frames
917
938
  * {stream, data}) with `!markPrice@arr@1s` (every symbol, 1 s) + `!ticker@arr` (only changed symbols).
918
939
  * r = accruing current-period rate (Phase 0: == premiumIndex.lastFundingRate ≠ last settled).
919
940
  * The funding interval is per market from the LABELLED REST source `rest:/fapi/v3/fundingInfo`