@xapy/orderbook 0.1.3 → 0.1.5

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 (69) hide show
  1. package/dist/exchanges/binance/index.cjs +605 -0
  2. package/dist/exchanges/binance/index.cjs.map +1 -0
  3. package/dist/exchanges/binance/index.d.cts +60 -0
  4. package/dist/exchanges/binance/index.d.ts +60 -0
  5. package/dist/exchanges/binance/index.js +601 -0
  6. package/dist/exchanges/binance/index.js.map +1 -0
  7. package/dist/exchanges/bingx/index.cjs +104 -24
  8. package/dist/exchanges/bingx/index.cjs.map +1 -1
  9. package/dist/exchanges/bingx/index.d.cts +10 -3
  10. package/dist/exchanges/bingx/index.d.ts +10 -3
  11. package/dist/exchanges/bingx/index.js +104 -24
  12. package/dist/exchanges/bingx/index.js.map +1 -1
  13. package/dist/exchanges/bitget/index.cjs +2 -1
  14. package/dist/exchanges/bitget/index.cjs.map +1 -1
  15. package/dist/exchanges/bitget/index.d.cts +1 -1
  16. package/dist/exchanges/bitget/index.d.ts +1 -1
  17. package/dist/exchanges/bitget/index.js +2 -1
  18. package/dist/exchanges/bitget/index.js.map +1 -1
  19. package/dist/exchanges/bybit/index.cjs +2 -1
  20. package/dist/exchanges/bybit/index.cjs.map +1 -1
  21. package/dist/exchanges/bybit/index.d.cts +1 -1
  22. package/dist/exchanges/bybit/index.d.ts +1 -1
  23. package/dist/exchanges/bybit/index.js +2 -1
  24. package/dist/exchanges/bybit/index.js.map +1 -1
  25. package/dist/exchanges/coinex/index.cjs +2 -1
  26. package/dist/exchanges/coinex/index.cjs.map +1 -1
  27. package/dist/exchanges/coinex/index.d.cts +1 -1
  28. package/dist/exchanges/coinex/index.d.ts +1 -1
  29. package/dist/exchanges/coinex/index.js +2 -1
  30. package/dist/exchanges/coinex/index.js.map +1 -1
  31. package/dist/exchanges/gate/index.cjs +2 -1
  32. package/dist/exchanges/gate/index.cjs.map +1 -1
  33. package/dist/exchanges/gate/index.d.cts +1 -1
  34. package/dist/exchanges/gate/index.d.ts +1 -1
  35. package/dist/exchanges/gate/index.js +2 -1
  36. package/dist/exchanges/gate/index.js.map +1 -1
  37. package/dist/exchanges/huobi/index.cjs +2 -1
  38. package/dist/exchanges/huobi/index.cjs.map +1 -1
  39. package/dist/exchanges/huobi/index.d.cts +1 -1
  40. package/dist/exchanges/huobi/index.d.ts +1 -1
  41. package/dist/exchanges/huobi/index.js +2 -1
  42. package/dist/exchanges/huobi/index.js.map +1 -1
  43. package/dist/exchanges/hyperliquid/index.cjs +536 -0
  44. package/dist/exchanges/hyperliquid/index.cjs.map +1 -0
  45. package/dist/exchanges/hyperliquid/index.d.cts +45 -0
  46. package/dist/exchanges/hyperliquid/index.d.ts +45 -0
  47. package/dist/exchanges/hyperliquid/index.js +532 -0
  48. package/dist/exchanges/hyperliquid/index.js.map +1 -0
  49. package/dist/exchanges/kucoin/index.cjs +614 -0
  50. package/dist/exchanges/kucoin/index.cjs.map +1 -0
  51. package/dist/exchanges/kucoin/index.d.cts +60 -0
  52. package/dist/exchanges/kucoin/index.d.ts +60 -0
  53. package/dist/exchanges/kucoin/index.js +609 -0
  54. package/dist/exchanges/kucoin/index.js.map +1 -0
  55. package/dist/exchanges/okx/index.cjs +2 -1
  56. package/dist/exchanges/okx/index.cjs.map +1 -1
  57. package/dist/exchanges/okx/index.d.cts +1 -1
  58. package/dist/exchanges/okx/index.d.ts +1 -1
  59. package/dist/exchanges/okx/index.js +2 -1
  60. package/dist/exchanges/okx/index.js.map +1 -1
  61. package/dist/index.cjs +729 -24
  62. package/dist/index.cjs.map +1 -1
  63. package/dist/index.d.cts +5 -2
  64. package/dist/index.d.ts +5 -2
  65. package/dist/index.js +727 -25
  66. package/dist/index.js.map +1 -1
  67. package/dist/{stream-BKpWmRYN.d.cts → stream-C73HPYqT.d.cts} +1 -1
  68. package/dist/{stream-BKpWmRYN.d.ts → stream-C73HPYqT.d.ts} +1 -1
  69. package/package.json +34 -1
package/dist/index.cjs CHANGED
@@ -358,7 +358,8 @@ var WSClient = class extends TypedEmitter {
358
358
  async connect() {
359
359
  if (this.closed) return;
360
360
  const WS = await getWebSocketCtor();
361
- const ws = new WS(this.cfg.url);
361
+ const url = typeof this.cfg.url === "function" ? await this.cfg.url() : this.cfg.url;
362
+ const ws = new WS(url);
362
363
  ws.binaryType = "arraybuffer";
363
364
  this.ws = ws;
364
365
  ws.onopen = async () => {
@@ -1233,29 +1234,50 @@ function streamBingxOrderbook(opts) {
1233
1234
  const wsSymbol = opts.symbol.toUpperCase().replace("/", "-");
1234
1235
  const level = opts.depth ?? 20;
1235
1236
  const interval = opts.interval ?? "500ms";
1236
- const dataType = `${wsSymbol}@depth${level}@${interval}`;
1237
+ const depthDataType = `${wsSymbol}@depth${level}@${interval}`;
1238
+ const tickerDataType = `${wsSymbol}@bookTicker`;
1237
1239
  const merger = new BookMerger();
1238
1240
  let ws = null;
1239
1241
  const stream = new OrderbookStream(() => ws?.close());
1240
- const subscribePayload = JSON.stringify({
1241
- id: `sub-${Date.now()}`,
1242
+ const subId = Date.now();
1243
+ const subscribeDepth = JSON.stringify({
1244
+ id: `sub-depth-${subId}`,
1242
1245
  reqType: "sub",
1243
- dataType
1246
+ dataType: depthDataType
1244
1247
  });
1245
- const handlePayload = (text) => {
1246
- if (text === "Ping" || text === "ping") {
1247
- ws?.send(text === "Ping" ? "Pong" : "pong");
1248
- return;
1249
- }
1250
- let msg;
1251
- try {
1252
- msg = JSON.parse(text);
1253
- } catch {
1254
- return;
1248
+ const subscribeTicker = JSON.stringify({
1249
+ id: `sub-ticker-${subId}`,
1250
+ reqType: "sub",
1251
+ dataType: tickerDataType
1252
+ });
1253
+ let lastMerged = null;
1254
+ let lastTicker = null;
1255
+ const emitOverlayed = () => {
1256
+ if (!lastMerged) return;
1257
+ let bids = lastMerged.bids;
1258
+ let asks = lastMerged.asks;
1259
+ let timestamp = lastMerged.timestamp;
1260
+ let sequence = lastMerged.sequence;
1261
+ if (lastTicker) {
1262
+ bids = overlayBid(bids, lastTicker.bid);
1263
+ asks = overlayAsk(asks, lastTicker.ask);
1264
+ if (lastTicker.ts > timestamp) {
1265
+ timestamp = lastTicker.ts;
1266
+ sequence = Math.max(sequence, lastTicker.ts);
1267
+ }
1255
1268
  }
1256
- if (msg.id && !msg.data) return;
1257
- if (msg.dataType !== dataType || !msg.data) return;
1258
- const d = msg.data;
1269
+ const book = {
1270
+ exchange: "bingx",
1271
+ symbol: opts.symbol,
1272
+ market: opts.market,
1273
+ bids,
1274
+ asks,
1275
+ timestamp,
1276
+ sequence
1277
+ };
1278
+ stream.emit("update", book);
1279
+ };
1280
+ const handleDepth = (d) => {
1259
1281
  const ts = d.T ?? d.ts ?? Date.now();
1260
1282
  const event = {
1261
1283
  kind: "snapshot",
@@ -1266,16 +1288,45 @@ function streamBingxOrderbook(opts) {
1266
1288
  };
1267
1289
  const r = merger.apply(event);
1268
1290
  if (!r.ok) return;
1269
- const book = {
1270
- exchange: "bingx",
1271
- symbol: opts.symbol,
1272
- market: opts.market,
1291
+ lastMerged = {
1273
1292
  bids: r.bids,
1274
1293
  asks: r.asks,
1275
1294
  timestamp: r.timestamp,
1276
1295
  sequence: r.sequence
1277
1296
  };
1278
- stream.emit("update", book);
1297
+ emitOverlayed();
1298
+ };
1299
+ const handleTicker = (t) => {
1300
+ const bidPrice = Number(t.b);
1301
+ const askPrice = Number(t.a);
1302
+ const bidSize = Number(t.B);
1303
+ const askSize = Number(t.A);
1304
+ if (!Number.isFinite(bidPrice) || !Number.isFinite(askPrice)) return;
1305
+ lastTicker = {
1306
+ bid: { price: bidPrice, size: bidSize },
1307
+ ask: { price: askPrice, size: askSize },
1308
+ ts: t.T ?? t.E ?? Date.now()
1309
+ };
1310
+ emitOverlayed();
1311
+ };
1312
+ const handlePayload = (text) => {
1313
+ if (text === "Ping" || text === "ping") {
1314
+ ws?.send(text === "Ping" ? "Pong" : "pong");
1315
+ return;
1316
+ }
1317
+ let msg;
1318
+ try {
1319
+ msg = JSON.parse(text);
1320
+ } catch {
1321
+ return;
1322
+ }
1323
+ if (msg.id && !msg.data) return;
1324
+ if (!msg.dataType || !msg.data) return;
1325
+ if (msg.dataType === depthDataType) {
1326
+ handleDepth(msg.data);
1327
+ } else if (msg.dataType === tickerDataType) {
1328
+ handleTicker(msg.data);
1329
+ }
1279
1330
  };
1280
1331
  const onMessage = async (raw) => {
1281
1332
  if (typeof raw === "string") {
@@ -1289,7 +1340,10 @@ function streamBingxOrderbook(opts) {
1289
1340
  url: isSpot ? WS_SPOT3 : WS_SWAP,
1290
1341
  onOpen: (sock) => {
1291
1342
  merger.reset();
1292
- sock.send(subscribePayload);
1343
+ lastMerged = null;
1344
+ lastTicker = null;
1345
+ sock.send(subscribeDepth);
1346
+ sock.send(subscribeTicker);
1293
1347
  },
1294
1348
  onMessage
1295
1349
  // bingx server is the one that sends Ping frames; we don't need client pings.
@@ -1301,6 +1355,32 @@ function streamBingxOrderbook(opts) {
1301
1355
  ws.connect().catch((e) => stream.emit("error", e));
1302
1356
  return stream;
1303
1357
  }
1358
+ function overlayAsk(asks, top) {
1359
+ if (!(top.size > 0)) return asks;
1360
+ let i = 0;
1361
+ for (; i < asks.length; i++) {
1362
+ const level = asks[i];
1363
+ if (!level || level.price >= top.price) break;
1364
+ }
1365
+ const at = asks[i];
1366
+ if (at && at.price === top.price) {
1367
+ return [{ price: top.price, size: top.size }, ...asks.slice(i + 1)];
1368
+ }
1369
+ return [{ price: top.price, size: top.size }, ...asks.slice(i)];
1370
+ }
1371
+ function overlayBid(bids, top) {
1372
+ if (!(top.size > 0)) return bids;
1373
+ let i = 0;
1374
+ for (; i < bids.length; i++) {
1375
+ const level = bids[i];
1376
+ if (!level || level.price <= top.price) break;
1377
+ }
1378
+ const at = bids[i];
1379
+ if (at && at.price === top.price) {
1380
+ return [{ price: top.price, size: top.size }, ...bids.slice(i + 1)];
1381
+ }
1382
+ return [{ price: top.price, size: top.size }, ...bids.slice(i)];
1383
+ }
1304
1384
 
1305
1385
  // src/exchanges/bingx/index.ts
1306
1386
  var BingxClient = class {
@@ -1835,7 +1915,630 @@ var OkxClient = class {
1835
1915
  }
1836
1916
  };
1837
1917
 
1918
+ // src/exchanges/binance/symbols.ts
1919
+ var QUOTES5 = [
1920
+ "USDT",
1921
+ "USDC",
1922
+ "FDUSD",
1923
+ "BUSD",
1924
+ "TUSD",
1925
+ "USD",
1926
+ "BTC",
1927
+ "ETH",
1928
+ "BNB"
1929
+ ];
1930
+ function toBinanceSymbol(symbol) {
1931
+ const [base, quote] = symbol.split("/");
1932
+ if (!base || !quote) {
1933
+ throw new Error(`invalid symbol "${symbol}" \u2014 expected "BASE/QUOTE"`);
1934
+ }
1935
+ return `${base}${quote}`.toUpperCase();
1936
+ }
1937
+ function fromBinanceSymbol(s) {
1938
+ const upper = s.toUpperCase();
1939
+ for (const q of QUOTES5) {
1940
+ if (upper.endsWith(q) && upper.length > q.length) {
1941
+ return `${upper.slice(0, -q.length)}/${q}`;
1942
+ }
1943
+ }
1944
+ return upper;
1945
+ }
1946
+
1947
+ // src/exchanges/binance/rest.ts
1948
+ var BASE_SPOT = "https://api.binance.com";
1949
+ var BASE_FAPI = "https://fapi.binance.com";
1950
+ async function fetchBinanceOrderbook(opts) {
1951
+ const symbol = toBinanceSymbol(opts.symbol);
1952
+ const limit = opts.depth ?? 1e3;
1953
+ const url = opts.market === "spot" ? `${BASE_SPOT}/api/v3/depth?symbol=${symbol}&limit=${limit}` : `${BASE_FAPI}/fapi/v1/depth?symbol=${symbol}&limit=${limit}`;
1954
+ const data = await httpJson({
1955
+ url,
1956
+ timeoutMs: opts.timeoutMs,
1957
+ transformUrl: opts.transformUrl
1958
+ });
1959
+ return {
1960
+ exchange: "binance",
1961
+ symbol: fromBinanceSymbol(symbol),
1962
+ market: opts.market,
1963
+ bids: data.bids.map(([p, s]) => ({ price: Number(p), size: Number(s) })),
1964
+ asks: data.asks.map(([p, s]) => ({ price: Number(p), size: Number(s) })),
1965
+ timestamp: data.E ?? data.T ?? Date.now(),
1966
+ sequence: data.lastUpdateId
1967
+ };
1968
+ }
1969
+
1970
+ // src/exchanges/binance/ws.ts
1971
+ var WS_SPOT6 = "wss://stream.binance.com:9443/ws";
1972
+ var WS_FUTURES3 = "wss://fstream.binance.com/ws";
1973
+ function streamBinanceOrderbook(opts) {
1974
+ const symbol = toBinanceSymbol(opts.symbol);
1975
+ const isSpot = opts.market === "spot";
1976
+ const interval = opts.interval ?? "100ms";
1977
+ const wsBase = isSpot ? WS_SPOT6 : WS_FUTURES3;
1978
+ const wsUrl = `${wsBase}/${symbol.toLowerCase()}@depth@${interval}`;
1979
+ const merger = new BookMerger();
1980
+ let snapshotId = -1;
1981
+ let lastU = -1;
1982
+ let snapshotInFlight = false;
1983
+ let snapshotDone = false;
1984
+ let buffered = [];
1985
+ let recovering = false;
1986
+ let ws = null;
1987
+ const stream = new OrderbookStream(() => ws?.close());
1988
+ const emit = (r) => {
1989
+ const book = {
1990
+ exchange: "binance",
1991
+ symbol: fromBinanceSymbol(symbol),
1992
+ market: opts.market,
1993
+ bids: r.bids,
1994
+ asks: r.asks,
1995
+ timestamp: r.timestamp,
1996
+ sequence: r.sequence
1997
+ };
1998
+ stream.emit("update", book);
1999
+ };
2000
+ const triggerSnapshot = () => {
2001
+ if (snapshotInFlight) return;
2002
+ snapshotInFlight = true;
2003
+ fetchBinanceOrderbook({
2004
+ symbol: opts.symbol,
2005
+ market: opts.market,
2006
+ depth: opts.depth ?? 1e3,
2007
+ timeoutMs: opts.timeoutMs,
2008
+ transformUrl: opts.transformUrl
2009
+ }).then((snap) => {
2010
+ snapshotId = snap.sequence;
2011
+ const r = merger.apply({
2012
+ kind: "snapshot",
2013
+ bids: snap.bids,
2014
+ asks: snap.asks,
2015
+ sequence: snap.sequence,
2016
+ timestamp: snap.timestamp
2017
+ });
2018
+ if (r.ok) emit(r);
2019
+ snapshotDone = true;
2020
+ lastU = -1;
2021
+ const pending2 = buffered;
2022
+ buffered = [];
2023
+ for (const ev of pending2) processDelta(ev);
2024
+ }).catch((err) => stream.emit("error", err)).finally(() => {
2025
+ snapshotInFlight = false;
2026
+ });
2027
+ };
2028
+ const resync = () => {
2029
+ if (recovering) return;
2030
+ recovering = true;
2031
+ merger.reset();
2032
+ snapshotId = -1;
2033
+ snapshotDone = false;
2034
+ lastU = -1;
2035
+ buffered = [];
2036
+ setTimeout(() => {
2037
+ recovering = false;
2038
+ triggerSnapshot();
2039
+ }, 100);
2040
+ };
2041
+ const processDelta = (ev) => {
2042
+ if (ev.u < snapshotId + 1) return;
2043
+ const bids = ev.b.map(([p, s]) => ({
2044
+ price: Number(p),
2045
+ size: Number(s)
2046
+ }));
2047
+ const asks = ev.a.map(([p, s]) => ({
2048
+ price: Number(p),
2049
+ size: Number(s)
2050
+ }));
2051
+ let prevSequence;
2052
+ if (lastU < 0) {
2053
+ const overlap = isSpot ? ev.U <= snapshotId + 1 : ev.U <= snapshotId;
2054
+ if (!overlap) {
2055
+ resync();
2056
+ return;
2057
+ }
2058
+ prevSequence = snapshotId;
2059
+ } else if (isSpot) {
2060
+ prevSequence = ev.U - 1;
2061
+ } else {
2062
+ if (ev.pu === void 0) {
2063
+ resync();
2064
+ return;
2065
+ }
2066
+ prevSequence = ev.pu;
2067
+ }
2068
+ const r = merger.apply({
2069
+ kind: "delta",
2070
+ bids,
2071
+ asks,
2072
+ sequence: ev.u,
2073
+ prevSequence,
2074
+ timestamp: ev.E
2075
+ });
2076
+ if (r.ok) {
2077
+ lastU = ev.u;
2078
+ emit(r);
2079
+ } else if (r.reason === "gap" || r.reason === "no-snapshot") {
2080
+ resync();
2081
+ }
2082
+ };
2083
+ const onMessage = (raw) => {
2084
+ if (typeof raw !== "string") return;
2085
+ let msg;
2086
+ try {
2087
+ msg = JSON.parse(raw);
2088
+ } catch {
2089
+ return;
2090
+ }
2091
+ if (msg.e !== "depthUpdate") return;
2092
+ if (!snapshotDone) {
2093
+ buffered.push(msg);
2094
+ if (!snapshotInFlight) triggerSnapshot();
2095
+ return;
2096
+ }
2097
+ processDelta(msg);
2098
+ };
2099
+ ws = new WSClient({
2100
+ url: wsUrl,
2101
+ onOpen: () => {
2102
+ merger.reset();
2103
+ snapshotId = -1;
2104
+ snapshotDone = false;
2105
+ lastU = -1;
2106
+ buffered = [];
2107
+ },
2108
+ onMessage
2109
+ // Binance server-initiated WS-level ping; auto-handled by the WS impl.
2110
+ });
2111
+ ws.on("open", () => stream.emit("connected"));
2112
+ ws.on("close", (r) => stream.emit("disconnected", r));
2113
+ ws.on("reconnecting", (a, w) => stream.emit("reconnecting", a, w));
2114
+ ws.on("error", (e) => stream.emit("error", e));
2115
+ ws.connect().catch((e) => stream.emit("error", e));
2116
+ return stream;
2117
+ }
2118
+
2119
+ // src/exchanges/binance/index.ts
2120
+ var BinanceClient = class {
2121
+ exchange = "binance";
2122
+ transformUrl;
2123
+ timeoutMs;
2124
+ constructor(opts = {}) {
2125
+ this.transformUrl = opts.transformUrl;
2126
+ this.timeoutMs = opts.timeoutMs;
2127
+ }
2128
+ fetchOrderbook(symbol, opts = {}) {
2129
+ const { pair, market } = parseSymbol(symbol);
2130
+ return fetchBinanceOrderbook({
2131
+ symbol: pair,
2132
+ market,
2133
+ depth: opts.depth,
2134
+ timeoutMs: this.timeoutMs,
2135
+ transformUrl: this.transformUrl
2136
+ });
2137
+ }
2138
+ streamOrderbook(symbol, opts = {}) {
2139
+ const { pair, market } = parseSymbol(symbol);
2140
+ return streamBinanceOrderbook({
2141
+ symbol: pair,
2142
+ market,
2143
+ depth: opts.depth,
2144
+ transformUrl: this.transformUrl,
2145
+ timeoutMs: this.timeoutMs
2146
+ });
2147
+ }
2148
+ };
2149
+
2150
+ // src/exchanges/hyperliquid/symbols.ts
2151
+ function toHyperliquidSymbol(symbol) {
2152
+ const [base, quote] = symbol.split("/");
2153
+ if (!base || !quote) {
2154
+ throw new Error(`invalid symbol "${symbol}" \u2014 expected "BASE/QUOTE"`);
2155
+ }
2156
+ if (quote.toUpperCase() !== "USDC") {
2157
+ throw new Error(
2158
+ `hyperliquid only supports USDC-quoted pairs; got "${quote}"`
2159
+ );
2160
+ }
2161
+ return base.toUpperCase();
2162
+ }
2163
+ function fromHyperliquidSymbol(coin) {
2164
+ return `${coin.toUpperCase()}/USDC`;
2165
+ }
2166
+
2167
+ // src/exchanges/hyperliquid/rest.ts
2168
+ var BASE7 = "https://api.hyperliquid.xyz";
2169
+ async function fetchHyperliquidOrderbook(opts) {
2170
+ if (opts.market !== "perpetual") {
2171
+ throw new Error(
2172
+ "hyperliquid: only perpetual markets are supported (spot uses @N-indexed coins)"
2173
+ );
2174
+ }
2175
+ const coin = toHyperliquidSymbol(opts.symbol);
2176
+ const data = await httpJson({
2177
+ url: `${BASE7}/info`,
2178
+ method: "POST",
2179
+ headers: { "Content-Type": "application/json" },
2180
+ body: JSON.stringify({ type: "l2Book", coin }),
2181
+ timeoutMs: opts.timeoutMs,
2182
+ transformUrl: opts.transformUrl
2183
+ });
2184
+ return {
2185
+ exchange: "hyperliquid",
2186
+ symbol: fromHyperliquidSymbol(data.coin),
2187
+ market: "perpetual",
2188
+ bids: data.levels[0].map((l) => ({
2189
+ price: Number(l.px),
2190
+ size: Number(l.sz)
2191
+ })),
2192
+ asks: data.levels[1].map((l) => ({
2193
+ price: Number(l.px),
2194
+ size: Number(l.sz)
2195
+ })),
2196
+ timestamp: data.time,
2197
+ sequence: data.time
2198
+ };
2199
+ }
2200
+
2201
+ // src/exchanges/hyperliquid/ws.ts
2202
+ var WS_URL = "wss://api.hyperliquid.xyz/ws";
2203
+ function streamHyperliquidOrderbook(opts) {
2204
+ if (opts.market !== "perpetual") {
2205
+ const s = new OrderbookStream(() => void 0);
2206
+ queueMicrotask(
2207
+ () => s.emit(
2208
+ "error",
2209
+ new Error(
2210
+ "hyperliquid: only perpetual markets are supported (spot uses @N-indexed coins)"
2211
+ )
2212
+ )
2213
+ );
2214
+ return s;
2215
+ }
2216
+ const coin = toHyperliquidSymbol(opts.symbol);
2217
+ const merger = new BookMerger();
2218
+ let ws = null;
2219
+ const stream = new OrderbookStream(() => ws?.close());
2220
+ const subscribePayload = JSON.stringify({
2221
+ method: "subscribe",
2222
+ subscription: { type: "l2Book", coin }
2223
+ });
2224
+ const onMessage = (raw) => {
2225
+ if (typeof raw !== "string") return;
2226
+ let msg;
2227
+ try {
2228
+ msg = JSON.parse(raw);
2229
+ } catch {
2230
+ return;
2231
+ }
2232
+ if (msg.channel === "pong" || msg.channel === "subscriptionResponse") {
2233
+ return;
2234
+ }
2235
+ if (msg.channel !== "l2Book" || !msg.data) return;
2236
+ const book = msg.data;
2237
+ if (book.coin !== coin) return;
2238
+ const event = {
2239
+ kind: "snapshot",
2240
+ bids: book.levels[0].map((l) => ({
2241
+ price: Number(l.px),
2242
+ size: Number(l.sz)
2243
+ })),
2244
+ asks: book.levels[1].map((l) => ({
2245
+ price: Number(l.px),
2246
+ size: Number(l.sz)
2247
+ })),
2248
+ sequence: book.time,
2249
+ timestamp: book.time
2250
+ };
2251
+ const r = merger.apply(event);
2252
+ if (!r.ok) return;
2253
+ const out = {
2254
+ exchange: "hyperliquid",
2255
+ symbol: fromHyperliquidSymbol(coin),
2256
+ market: "perpetual",
2257
+ bids: r.bids,
2258
+ asks: r.asks,
2259
+ timestamp: r.timestamp,
2260
+ sequence: r.sequence
2261
+ };
2262
+ stream.emit("update", out);
2263
+ };
2264
+ ws = new WSClient({
2265
+ url: WS_URL,
2266
+ onOpen: (sock) => {
2267
+ merger.reset();
2268
+ sock.send(subscribePayload);
2269
+ },
2270
+ onMessage,
2271
+ pingIntervalMs: 5e4,
2272
+ pingPayload: () => JSON.stringify({ method: "ping" })
2273
+ });
2274
+ ws.on("open", () => stream.emit("connected"));
2275
+ ws.on("close", (r) => stream.emit("disconnected", r));
2276
+ ws.on("reconnecting", (a, w) => stream.emit("reconnecting", a, w));
2277
+ ws.on("error", (e) => stream.emit("error", e));
2278
+ ws.connect().catch((e) => stream.emit("error", e));
2279
+ return stream;
2280
+ }
2281
+
2282
+ // src/exchanges/hyperliquid/index.ts
2283
+ var HyperliquidClient = class {
2284
+ exchange = "hyperliquid";
2285
+ transformUrl;
2286
+ timeoutMs;
2287
+ constructor(opts = {}) {
2288
+ this.transformUrl = opts.transformUrl;
2289
+ this.timeoutMs = opts.timeoutMs;
2290
+ }
2291
+ fetchOrderbook(symbol, opts = {}) {
2292
+ const { pair, market } = parseSymbol(symbol);
2293
+ return fetchHyperliquidOrderbook({
2294
+ symbol: pair,
2295
+ market,
2296
+ depth: opts.depth,
2297
+ timeoutMs: this.timeoutMs,
2298
+ transformUrl: this.transformUrl
2299
+ });
2300
+ }
2301
+ streamOrderbook(symbol, opts = {}) {
2302
+ const { pair, market } = parseSymbol(symbol);
2303
+ return streamHyperliquidOrderbook({
2304
+ symbol: pair,
2305
+ market,
2306
+ depth: opts.depth,
2307
+ transformUrl: this.transformUrl,
2308
+ timeoutMs: this.timeoutMs
2309
+ });
2310
+ }
2311
+ };
2312
+
2313
+ // src/exchanges/kucoin/symbols.ts
2314
+ var FUTURES_QUOTES = ["USDT", "USDC", "USD"];
2315
+ function toKucoinSpotSymbol(symbol) {
2316
+ const [base, quote] = symbol.split("/");
2317
+ if (!base || !quote) {
2318
+ throw new Error(`invalid symbol "${symbol}" \u2014 expected "BASE/QUOTE"`);
2319
+ }
2320
+ return `${base.toUpperCase()}-${quote.toUpperCase()}`;
2321
+ }
2322
+ function fromKucoinSpotSymbol(s) {
2323
+ return s.toUpperCase().replace("-", "/");
2324
+ }
2325
+ function toKucoinFuturesSymbol(symbol) {
2326
+ const [base, quote] = symbol.split("/");
2327
+ if (!base || !quote) {
2328
+ throw new Error(`invalid symbol "${symbol}" \u2014 expected "BASE/QUOTE"`);
2329
+ }
2330
+ const b = base.toUpperCase() === "BTC" ? "XBT" : base.toUpperCase();
2331
+ return `${b}${quote.toUpperCase()}M`;
2332
+ }
2333
+ function fromKucoinFuturesSymbol(s) {
2334
+ const upper = s.toUpperCase();
2335
+ const stripped = upper.endsWith("M") ? upper.slice(0, -1) : upper;
2336
+ for (const q of FUTURES_QUOTES) {
2337
+ if (stripped.endsWith(q) && stripped.length > q.length) {
2338
+ let base = stripped.slice(0, -q.length);
2339
+ if (base === "XBT") base = "BTC";
2340
+ return `${base}/${q}`;
2341
+ }
2342
+ }
2343
+ return upper;
2344
+ }
2345
+
2346
+ // src/exchanges/kucoin/rest.ts
2347
+ var BASE_SPOT2 = "https://api.kucoin.com";
2348
+ var BASE_FUTURES = "https://api-futures.kucoin.com";
2349
+ async function fetchKucoinBullet(opts) {
2350
+ const base = opts.market === "spot" ? BASE_SPOT2 : BASE_FUTURES;
2351
+ const res = await httpJson({
2352
+ url: `${base}/api/v1/bullet-public`,
2353
+ method: "POST",
2354
+ timeoutMs: opts.timeoutMs,
2355
+ transformUrl: opts.transformUrl
2356
+ });
2357
+ if (res.code !== "200000") {
2358
+ throw new ExchangeError("kucoin", `bullet failed: ${res.code}`);
2359
+ }
2360
+ const server = res.data.instanceServers[0];
2361
+ if (!server) {
2362
+ throw new ExchangeError("kucoin", "bullet returned no instance servers");
2363
+ }
2364
+ return {
2365
+ endpoint: server.endpoint,
2366
+ token: res.data.token,
2367
+ pingInterval: server.pingInterval,
2368
+ pingTimeout: server.pingTimeout
2369
+ };
2370
+ }
2371
+ async function fetchKucoinOrderbook(opts) {
2372
+ const tier = (opts.depth ?? 100) <= 20 ? 20 : 100;
2373
+ if (opts.market === "spot") {
2374
+ const symbol2 = toKucoinSpotSymbol(opts.symbol);
2375
+ const url2 = `${BASE_SPOT2}/api/v1/market/orderbook/level2_${tier}?symbol=${symbol2}`;
2376
+ const res2 = await httpJson({
2377
+ url: url2,
2378
+ timeoutMs: opts.timeoutMs,
2379
+ transformUrl: opts.transformUrl
2380
+ });
2381
+ if (res2.code !== "200000") {
2382
+ throw new ExchangeError("kucoin", `orderbook failed: ${res2.code}`);
2383
+ }
2384
+ return {
2385
+ exchange: "kucoin",
2386
+ symbol: fromKucoinSpotSymbol(symbol2),
2387
+ market: "spot",
2388
+ bids: parseLevels(res2.data.bids),
2389
+ asks: parseLevels(res2.data.asks),
2390
+ timestamp: res2.data.time,
2391
+ sequence: Number(res2.data.sequence)
2392
+ };
2393
+ }
2394
+ const symbol = toKucoinFuturesSymbol(opts.symbol);
2395
+ const url = `${BASE_FUTURES}/api/v1/level2/depth${tier}?symbol=${symbol}`;
2396
+ const res = await httpJson({
2397
+ url,
2398
+ timeoutMs: opts.timeoutMs,
2399
+ transformUrl: opts.transformUrl
2400
+ });
2401
+ if (res.code !== "200000") {
2402
+ throw new ExchangeError("kucoin", `orderbook failed: ${res.code}`);
2403
+ }
2404
+ return {
2405
+ exchange: "kucoin",
2406
+ symbol: fromKucoinFuturesSymbol(symbol),
2407
+ market: "perpetual",
2408
+ bids: parseLevels(res.data.bids),
2409
+ asks: parseLevels(res.data.asks),
2410
+ timestamp: res.data.ts,
2411
+ sequence: res.data.sequence
2412
+ };
2413
+ }
2414
+ function parseLevels(rows) {
2415
+ return rows.map(([p, s]) => ({ price: Number(p), size: Number(s) }));
2416
+ }
2417
+
2418
+ // src/exchanges/kucoin/ws.ts
2419
+ function streamKucoinOrderbook(opts) {
2420
+ const isSpot = opts.market === "spot";
2421
+ const symbol = isSpot ? toKucoinSpotSymbol(opts.symbol) : toKucoinFuturesSymbol(opts.symbol);
2422
+ const level = (opts.depth ?? 50) <= 5 ? 5 : 50;
2423
+ const topic = isSpot ? `/spotMarket/level2Depth${level}:${symbol}` : `/contractMarket/level2Depth${level}:${symbol}`;
2424
+ const merger = new BookMerger();
2425
+ let ws = null;
2426
+ const stream = new OrderbookStream(() => ws?.close());
2427
+ const urlProvider = async () => {
2428
+ const bullet = await fetchKucoinBullet({
2429
+ market: opts.market,
2430
+ timeoutMs: opts.timeoutMs,
2431
+ transformUrl: opts.transformUrl
2432
+ });
2433
+ const connectId = `xapy-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
2434
+ return `${bullet.endpoint}?token=${encodeURIComponent(bullet.token)}&connectId=${connectId}`;
2435
+ };
2436
+ const subscribePayload = () => JSON.stringify({
2437
+ id: `sub-${Date.now()}`,
2438
+ type: "subscribe",
2439
+ topic,
2440
+ privateChannel: false,
2441
+ response: true
2442
+ });
2443
+ const pingPayload = () => JSON.stringify({ id: `${Date.now()}`, type: "ping" });
2444
+ const handleDepth = (d) => {
2445
+ const ts = d.timestamp ?? d.ts ?? Date.now();
2446
+ const event = {
2447
+ kind: "snapshot",
2448
+ bids: (d.bids ?? []).map(([p, s]) => ({
2449
+ price: Number(p),
2450
+ size: Number(s)
2451
+ })),
2452
+ asks: (d.asks ?? []).map(([p, s]) => ({
2453
+ price: Number(p),
2454
+ size: Number(s)
2455
+ })),
2456
+ sequence: ts,
2457
+ timestamp: ts
2458
+ };
2459
+ const r = merger.apply(event);
2460
+ if (!r.ok) return;
2461
+ const book = {
2462
+ exchange: "kucoin",
2463
+ symbol: isSpot ? fromKucoinSpotSymbol(symbol) : fromKucoinFuturesSymbol(symbol),
2464
+ market: opts.market,
2465
+ bids: r.bids,
2466
+ asks: r.asks,
2467
+ timestamp: r.timestamp,
2468
+ sequence: r.sequence
2469
+ };
2470
+ stream.emit("update", book);
2471
+ };
2472
+ const onMessage = (raw) => {
2473
+ if (typeof raw !== "string") return;
2474
+ let msg;
2475
+ try {
2476
+ msg = JSON.parse(raw);
2477
+ } catch {
2478
+ return;
2479
+ }
2480
+ if (msg.type === "welcome" || msg.type === "ack" || msg.type === "pong") {
2481
+ return;
2482
+ }
2483
+ if (msg.type === "error") {
2484
+ stream.emit("error", new Error(`kucoin: ${JSON.stringify(msg)}`));
2485
+ return;
2486
+ }
2487
+ if (msg.type !== "message" || msg.topic !== topic || !msg.data) return;
2488
+ handleDepth(msg.data);
2489
+ };
2490
+ ws = new WSClient({
2491
+ url: urlProvider,
2492
+ onOpen: (sock) => {
2493
+ merger.reset();
2494
+ sock.send(subscribePayload());
2495
+ },
2496
+ onMessage,
2497
+ // KuCoin's documented bullet `pingInterval` is 18000ms — stay under it.
2498
+ pingIntervalMs: 15e3,
2499
+ pingPayload
2500
+ });
2501
+ ws.on("open", () => stream.emit("connected"));
2502
+ ws.on("close", (r) => stream.emit("disconnected", r));
2503
+ ws.on("reconnecting", (a, w) => stream.emit("reconnecting", a, w));
2504
+ ws.on("error", (e) => stream.emit("error", e));
2505
+ ws.connect().catch((e) => stream.emit("error", e));
2506
+ return stream;
2507
+ }
2508
+
2509
+ // src/exchanges/kucoin/index.ts
2510
+ var KucoinClient = class {
2511
+ exchange = "kucoin";
2512
+ transformUrl;
2513
+ timeoutMs;
2514
+ constructor(opts = {}) {
2515
+ this.transformUrl = opts.transformUrl;
2516
+ this.timeoutMs = opts.timeoutMs;
2517
+ }
2518
+ fetchOrderbook(symbol, opts = {}) {
2519
+ const { pair, market } = parseSymbol(symbol);
2520
+ return fetchKucoinOrderbook({
2521
+ symbol: pair,
2522
+ market,
2523
+ depth: opts.depth,
2524
+ timeoutMs: this.timeoutMs,
2525
+ transformUrl: this.transformUrl
2526
+ });
2527
+ }
2528
+ streamOrderbook(symbol, opts = {}) {
2529
+ const { pair, market } = parseSymbol(symbol);
2530
+ return streamKucoinOrderbook({
2531
+ symbol: pair,
2532
+ market,
2533
+ depth: opts.depth,
2534
+ transformUrl: this.transformUrl,
2535
+ timeoutMs: this.timeoutMs
2536
+ });
2537
+ }
2538
+ };
2539
+
1838
2540
  exports.Backoff = Backoff;
2541
+ exports.BinanceClient = BinanceClient;
1839
2542
  exports.BingxClient = BingxClient;
1840
2543
  exports.BitgetClient = BitgetClient;
1841
2544
  exports.BookMerger = BookMerger;
@@ -1844,6 +2547,8 @@ exports.CoinexClient = CoinexClient;
1844
2547
  exports.ExchangeError = ExchangeError;
1845
2548
  exports.GateClient = GateClient;
1846
2549
  exports.HuobiClient = HuobiClient;
2550
+ exports.HyperliquidClient = HyperliquidClient;
2551
+ exports.KucoinClient = KucoinClient;
1847
2552
  exports.OkxClient = OkxClient;
1848
2553
  exports.OrderbookStream = OrderbookStream;
1849
2554
  exports.RateLimitError = RateLimitError;