@xapy/orderbook 0.1.4 → 0.1.6

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 +2 -1
  8. package/dist/exchanges/bingx/index.cjs.map +1 -1
  9. package/dist/exchanges/bingx/index.d.cts +1 -1
  10. package/dist/exchanges/bingx/index.d.ts +1 -1
  11. package/dist/exchanges/bingx/index.js +2 -1
  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 +111 -24
  26. package/dist/exchanges/coinex/index.cjs.map +1 -1
  27. package/dist/exchanges/coinex/index.d.cts +13 -5
  28. package/dist/exchanges/coinex/index.d.ts +13 -5
  29. package/dist/exchanges/coinex/index.js +111 -24
  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 +736 -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 +734 -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 () => {
@@ -1625,44 +1626,101 @@ function streamCoinexOrderbook(opts) {
1625
1626
  const merger = new BookMerger();
1626
1627
  let ws = null;
1627
1628
  const stream = new OrderbookStream(() => ws?.close());
1628
- const subscribePayload = JSON.stringify({
1629
+ const subscribeDepth = JSON.stringify({
1629
1630
  method: "depth.subscribe",
1630
1631
  params: {
1631
- // [market, limit, interval, diff_flag] — diff_flag=falsefull snapshots
1632
+ // [market, limit, interval, diff_flag] — diff_flag=truesnapshots only
1633
+ // (CoinEx flips the sense of the flag in V2; our handler treats every
1634
+ // frame as a snapshot to stay correct under either reading).
1632
1635
  market_list: [[market, depth, "0", true]]
1633
1636
  },
1634
1637
  id: 1
1635
1638
  });
1636
- const handlePayload = (text) => {
1637
- let msg;
1638
- try {
1639
- msg = JSON.parse(text);
1640
- } catch {
1641
- return;
1639
+ const subscribeBbo = JSON.stringify({
1640
+ method: "bbo.subscribe",
1641
+ params: { market_list: [market] },
1642
+ id: 2
1643
+ });
1644
+ let lastMerged = null;
1645
+ let lastTicker = null;
1646
+ const emitOverlayed = () => {
1647
+ if (!lastMerged) return;
1648
+ let bids = lastMerged.bids;
1649
+ let asks = lastMerged.asks;
1650
+ let timestamp = lastMerged.timestamp;
1651
+ let sequence = lastMerged.sequence;
1652
+ if (lastTicker) {
1653
+ bids = overlayBid2(bids, lastTicker.bid);
1654
+ asks = overlayAsk2(asks, lastTicker.ask);
1655
+ if (lastTicker.ts > timestamp) {
1656
+ timestamp = lastTicker.ts;
1657
+ sequence = Math.max(sequence, lastTicker.ts);
1658
+ }
1642
1659
  }
1643
- if (msg.id != null && !msg.method) return;
1644
- if (msg.method !== "depth.update" || !msg.data) return;
1645
- if (msg.data.market !== market) return;
1646
- const d = msg.data.depth;
1647
- const event = {
1648
- kind: "snapshot",
1649
- bids: d.bids.map(([p, s]) => ({ price: Number(p), size: Number(s) })),
1650
- asks: d.asks.map(([p, s]) => ({ price: Number(p), size: Number(s) })),
1651
- sequence: d.updated_at,
1652
- timestamp: d.updated_at
1653
- };
1654
- const r = merger.apply(event);
1655
- if (!r.ok) return;
1656
1660
  const book = {
1657
1661
  exchange: "coinex",
1658
1662
  symbol: fromCoinexSymbol(market),
1659
1663
  market: opts.market,
1664
+ bids,
1665
+ asks,
1666
+ timestamp,
1667
+ sequence
1668
+ };
1669
+ stream.emit("update", book);
1670
+ };
1671
+ const handleDepth = (d) => {
1672
+ if (d.market !== market) return;
1673
+ const inner = d.depth;
1674
+ const r = merger.apply({
1675
+ kind: "snapshot",
1676
+ bids: inner.bids.map(([p, s]) => ({
1677
+ price: Number(p),
1678
+ size: Number(s)
1679
+ })),
1680
+ asks: inner.asks.map(([p, s]) => ({
1681
+ price: Number(p),
1682
+ size: Number(s)
1683
+ })),
1684
+ sequence: inner.updated_at,
1685
+ timestamp: inner.updated_at
1686
+ });
1687
+ if (!r.ok) return;
1688
+ lastMerged = {
1660
1689
  bids: r.bids,
1661
1690
  asks: r.asks,
1662
1691
  timestamp: r.timestamp,
1663
1692
  sequence: r.sequence
1664
1693
  };
1665
- stream.emit("update", book);
1694
+ emitOverlayed();
1695
+ };
1696
+ const handleBbo = (b) => {
1697
+ if (b.market !== market) return;
1698
+ const bidPrice = Number(b.best_bid_price);
1699
+ const askPrice = Number(b.best_ask_price);
1700
+ const bidSize = Number(b.best_bid_size);
1701
+ const askSize = Number(b.best_ask_size);
1702
+ if (!Number.isFinite(bidPrice) || !Number.isFinite(askPrice)) return;
1703
+ lastTicker = {
1704
+ bid: { price: bidPrice, size: bidSize },
1705
+ ask: { price: askPrice, size: askSize },
1706
+ ts: b.updated_at
1707
+ };
1708
+ emitOverlayed();
1709
+ };
1710
+ const handlePayload = (text) => {
1711
+ let msg;
1712
+ try {
1713
+ msg = JSON.parse(text);
1714
+ } catch {
1715
+ return;
1716
+ }
1717
+ if (msg.id != null && !msg.method) return;
1718
+ if (!msg.data || !msg.method) return;
1719
+ if (msg.method === "depth.update") {
1720
+ handleDepth(msg.data);
1721
+ } else if (msg.method === "bbo.update") {
1722
+ handleBbo(msg.data);
1723
+ }
1666
1724
  };
1667
1725
  const onMessage = async (raw) => {
1668
1726
  if (typeof raw === "string") {
@@ -1676,7 +1734,10 @@ function streamCoinexOrderbook(opts) {
1676
1734
  url: opts.market === "spot" ? WS_SPOT5 : WS_FUTURES2,
1677
1735
  onOpen: (sock) => {
1678
1736
  merger.reset();
1679
- sock.send(subscribePayload);
1737
+ lastMerged = null;
1738
+ lastTicker = null;
1739
+ sock.send(subscribeDepth);
1740
+ sock.send(subscribeBbo);
1680
1741
  },
1681
1742
  onMessage
1682
1743
  // Native WS ping/pong frames keep the connection alive — no app-level ping.
@@ -1688,6 +1749,32 @@ function streamCoinexOrderbook(opts) {
1688
1749
  ws.connect().catch((e) => stream.emit("error", e));
1689
1750
  return stream;
1690
1751
  }
1752
+ function overlayAsk2(asks, top) {
1753
+ if (!(top.size > 0)) return asks;
1754
+ let i = 0;
1755
+ for (; i < asks.length; i++) {
1756
+ const level = asks[i];
1757
+ if (!level || level.price >= top.price) break;
1758
+ }
1759
+ const at = asks[i];
1760
+ if (at && at.price === top.price) {
1761
+ return [{ price: top.price, size: top.size }, ...asks.slice(i + 1)];
1762
+ }
1763
+ return [{ price: top.price, size: top.size }, ...asks.slice(i)];
1764
+ }
1765
+ function overlayBid2(bids, top) {
1766
+ if (!(top.size > 0)) return bids;
1767
+ let i = 0;
1768
+ for (; i < bids.length; i++) {
1769
+ const level = bids[i];
1770
+ if (!level || level.price <= top.price) break;
1771
+ }
1772
+ const at = bids[i];
1773
+ if (at && at.price === top.price) {
1774
+ return [{ price: top.price, size: top.size }, ...bids.slice(i + 1)];
1775
+ }
1776
+ return [{ price: top.price, size: top.size }, ...bids.slice(i)];
1777
+ }
1691
1778
 
1692
1779
  // src/exchanges/coinex/index.ts
1693
1780
  var CoinexClient = class {
@@ -1914,7 +2001,630 @@ var OkxClient = class {
1914
2001
  }
1915
2002
  };
1916
2003
 
2004
+ // src/exchanges/binance/symbols.ts
2005
+ var QUOTES5 = [
2006
+ "USDT",
2007
+ "USDC",
2008
+ "FDUSD",
2009
+ "BUSD",
2010
+ "TUSD",
2011
+ "USD",
2012
+ "BTC",
2013
+ "ETH",
2014
+ "BNB"
2015
+ ];
2016
+ function toBinanceSymbol(symbol) {
2017
+ const [base, quote] = symbol.split("/");
2018
+ if (!base || !quote) {
2019
+ throw new Error(`invalid symbol "${symbol}" \u2014 expected "BASE/QUOTE"`);
2020
+ }
2021
+ return `${base}${quote}`.toUpperCase();
2022
+ }
2023
+ function fromBinanceSymbol(s) {
2024
+ const upper = s.toUpperCase();
2025
+ for (const q of QUOTES5) {
2026
+ if (upper.endsWith(q) && upper.length > q.length) {
2027
+ return `${upper.slice(0, -q.length)}/${q}`;
2028
+ }
2029
+ }
2030
+ return upper;
2031
+ }
2032
+
2033
+ // src/exchanges/binance/rest.ts
2034
+ var BASE_SPOT = "https://api.binance.com";
2035
+ var BASE_FAPI = "https://fapi.binance.com";
2036
+ async function fetchBinanceOrderbook(opts) {
2037
+ const symbol = toBinanceSymbol(opts.symbol);
2038
+ const limit = opts.depth ?? 1e3;
2039
+ const url = opts.market === "spot" ? `${BASE_SPOT}/api/v3/depth?symbol=${symbol}&limit=${limit}` : `${BASE_FAPI}/fapi/v1/depth?symbol=${symbol}&limit=${limit}`;
2040
+ const data = await httpJson({
2041
+ url,
2042
+ timeoutMs: opts.timeoutMs,
2043
+ transformUrl: opts.transformUrl
2044
+ });
2045
+ return {
2046
+ exchange: "binance",
2047
+ symbol: fromBinanceSymbol(symbol),
2048
+ market: opts.market,
2049
+ bids: data.bids.map(([p, s]) => ({ price: Number(p), size: Number(s) })),
2050
+ asks: data.asks.map(([p, s]) => ({ price: Number(p), size: Number(s) })),
2051
+ timestamp: data.E ?? data.T ?? Date.now(),
2052
+ sequence: data.lastUpdateId
2053
+ };
2054
+ }
2055
+
2056
+ // src/exchanges/binance/ws.ts
2057
+ var WS_SPOT6 = "wss://stream.binance.com:9443/ws";
2058
+ var WS_FUTURES3 = "wss://fstream.binance.com/ws";
2059
+ function streamBinanceOrderbook(opts) {
2060
+ const symbol = toBinanceSymbol(opts.symbol);
2061
+ const isSpot = opts.market === "spot";
2062
+ const interval = opts.interval ?? "100ms";
2063
+ const wsBase = isSpot ? WS_SPOT6 : WS_FUTURES3;
2064
+ const wsUrl = `${wsBase}/${symbol.toLowerCase()}@depth@${interval}`;
2065
+ const merger = new BookMerger();
2066
+ let snapshotId = -1;
2067
+ let lastU = -1;
2068
+ let snapshotInFlight = false;
2069
+ let snapshotDone = false;
2070
+ let buffered = [];
2071
+ let recovering = false;
2072
+ let ws = null;
2073
+ const stream = new OrderbookStream(() => ws?.close());
2074
+ const emit = (r) => {
2075
+ const book = {
2076
+ exchange: "binance",
2077
+ symbol: fromBinanceSymbol(symbol),
2078
+ market: opts.market,
2079
+ bids: r.bids,
2080
+ asks: r.asks,
2081
+ timestamp: r.timestamp,
2082
+ sequence: r.sequence
2083
+ };
2084
+ stream.emit("update", book);
2085
+ };
2086
+ const triggerSnapshot = () => {
2087
+ if (snapshotInFlight) return;
2088
+ snapshotInFlight = true;
2089
+ fetchBinanceOrderbook({
2090
+ symbol: opts.symbol,
2091
+ market: opts.market,
2092
+ depth: opts.depth ?? 1e3,
2093
+ timeoutMs: opts.timeoutMs,
2094
+ transformUrl: opts.transformUrl
2095
+ }).then((snap) => {
2096
+ snapshotId = snap.sequence;
2097
+ const r = merger.apply({
2098
+ kind: "snapshot",
2099
+ bids: snap.bids,
2100
+ asks: snap.asks,
2101
+ sequence: snap.sequence,
2102
+ timestamp: snap.timestamp
2103
+ });
2104
+ if (r.ok) emit(r);
2105
+ snapshotDone = true;
2106
+ lastU = -1;
2107
+ const pending2 = buffered;
2108
+ buffered = [];
2109
+ for (const ev of pending2) processDelta(ev);
2110
+ }).catch((err) => stream.emit("error", err)).finally(() => {
2111
+ snapshotInFlight = false;
2112
+ });
2113
+ };
2114
+ const resync = () => {
2115
+ if (recovering) return;
2116
+ recovering = true;
2117
+ merger.reset();
2118
+ snapshotId = -1;
2119
+ snapshotDone = false;
2120
+ lastU = -1;
2121
+ buffered = [];
2122
+ setTimeout(() => {
2123
+ recovering = false;
2124
+ triggerSnapshot();
2125
+ }, 100);
2126
+ };
2127
+ const processDelta = (ev) => {
2128
+ if (ev.u < snapshotId + 1) return;
2129
+ const bids = ev.b.map(([p, s]) => ({
2130
+ price: Number(p),
2131
+ size: Number(s)
2132
+ }));
2133
+ const asks = ev.a.map(([p, s]) => ({
2134
+ price: Number(p),
2135
+ size: Number(s)
2136
+ }));
2137
+ let prevSequence;
2138
+ if (lastU < 0) {
2139
+ const overlap = isSpot ? ev.U <= snapshotId + 1 : ev.U <= snapshotId;
2140
+ if (!overlap) {
2141
+ resync();
2142
+ return;
2143
+ }
2144
+ prevSequence = snapshotId;
2145
+ } else if (isSpot) {
2146
+ prevSequence = ev.U - 1;
2147
+ } else {
2148
+ if (ev.pu === void 0) {
2149
+ resync();
2150
+ return;
2151
+ }
2152
+ prevSequence = ev.pu;
2153
+ }
2154
+ const r = merger.apply({
2155
+ kind: "delta",
2156
+ bids,
2157
+ asks,
2158
+ sequence: ev.u,
2159
+ prevSequence,
2160
+ timestamp: ev.E
2161
+ });
2162
+ if (r.ok) {
2163
+ lastU = ev.u;
2164
+ emit(r);
2165
+ } else if (r.reason === "gap" || r.reason === "no-snapshot") {
2166
+ resync();
2167
+ }
2168
+ };
2169
+ const onMessage = (raw) => {
2170
+ if (typeof raw !== "string") return;
2171
+ let msg;
2172
+ try {
2173
+ msg = JSON.parse(raw);
2174
+ } catch {
2175
+ return;
2176
+ }
2177
+ if (msg.e !== "depthUpdate") return;
2178
+ if (!snapshotDone) {
2179
+ buffered.push(msg);
2180
+ if (!snapshotInFlight) triggerSnapshot();
2181
+ return;
2182
+ }
2183
+ processDelta(msg);
2184
+ };
2185
+ ws = new WSClient({
2186
+ url: wsUrl,
2187
+ onOpen: () => {
2188
+ merger.reset();
2189
+ snapshotId = -1;
2190
+ snapshotDone = false;
2191
+ lastU = -1;
2192
+ buffered = [];
2193
+ },
2194
+ onMessage
2195
+ // Binance server-initiated WS-level ping; auto-handled by the WS impl.
2196
+ });
2197
+ ws.on("open", () => stream.emit("connected"));
2198
+ ws.on("close", (r) => stream.emit("disconnected", r));
2199
+ ws.on("reconnecting", (a, w) => stream.emit("reconnecting", a, w));
2200
+ ws.on("error", (e) => stream.emit("error", e));
2201
+ ws.connect().catch((e) => stream.emit("error", e));
2202
+ return stream;
2203
+ }
2204
+
2205
+ // src/exchanges/binance/index.ts
2206
+ var BinanceClient = class {
2207
+ exchange = "binance";
2208
+ transformUrl;
2209
+ timeoutMs;
2210
+ constructor(opts = {}) {
2211
+ this.transformUrl = opts.transformUrl;
2212
+ this.timeoutMs = opts.timeoutMs;
2213
+ }
2214
+ fetchOrderbook(symbol, opts = {}) {
2215
+ const { pair, market } = parseSymbol(symbol);
2216
+ return fetchBinanceOrderbook({
2217
+ symbol: pair,
2218
+ market,
2219
+ depth: opts.depth,
2220
+ timeoutMs: this.timeoutMs,
2221
+ transformUrl: this.transformUrl
2222
+ });
2223
+ }
2224
+ streamOrderbook(symbol, opts = {}) {
2225
+ const { pair, market } = parseSymbol(symbol);
2226
+ return streamBinanceOrderbook({
2227
+ symbol: pair,
2228
+ market,
2229
+ depth: opts.depth,
2230
+ transformUrl: this.transformUrl,
2231
+ timeoutMs: this.timeoutMs
2232
+ });
2233
+ }
2234
+ };
2235
+
2236
+ // src/exchanges/hyperliquid/symbols.ts
2237
+ function toHyperliquidSymbol(symbol) {
2238
+ const [base, quote] = symbol.split("/");
2239
+ if (!base || !quote) {
2240
+ throw new Error(`invalid symbol "${symbol}" \u2014 expected "BASE/QUOTE"`);
2241
+ }
2242
+ if (quote.toUpperCase() !== "USDC") {
2243
+ throw new Error(
2244
+ `hyperliquid only supports USDC-quoted pairs; got "${quote}"`
2245
+ );
2246
+ }
2247
+ return base.toUpperCase();
2248
+ }
2249
+ function fromHyperliquidSymbol(coin) {
2250
+ return `${coin.toUpperCase()}/USDC`;
2251
+ }
2252
+
2253
+ // src/exchanges/hyperliquid/rest.ts
2254
+ var BASE7 = "https://api.hyperliquid.xyz";
2255
+ async function fetchHyperliquidOrderbook(opts) {
2256
+ if (opts.market !== "perpetual") {
2257
+ throw new Error(
2258
+ "hyperliquid: only perpetual markets are supported (spot uses @N-indexed coins)"
2259
+ );
2260
+ }
2261
+ const coin = toHyperliquidSymbol(opts.symbol);
2262
+ const data = await httpJson({
2263
+ url: `${BASE7}/info`,
2264
+ method: "POST",
2265
+ headers: { "Content-Type": "application/json" },
2266
+ body: JSON.stringify({ type: "l2Book", coin }),
2267
+ timeoutMs: opts.timeoutMs,
2268
+ transformUrl: opts.transformUrl
2269
+ });
2270
+ return {
2271
+ exchange: "hyperliquid",
2272
+ symbol: fromHyperliquidSymbol(data.coin),
2273
+ market: "perpetual",
2274
+ bids: data.levels[0].map((l) => ({
2275
+ price: Number(l.px),
2276
+ size: Number(l.sz)
2277
+ })),
2278
+ asks: data.levels[1].map((l) => ({
2279
+ price: Number(l.px),
2280
+ size: Number(l.sz)
2281
+ })),
2282
+ timestamp: data.time,
2283
+ sequence: data.time
2284
+ };
2285
+ }
2286
+
2287
+ // src/exchanges/hyperliquid/ws.ts
2288
+ var WS_URL = "wss://api.hyperliquid.xyz/ws";
2289
+ function streamHyperliquidOrderbook(opts) {
2290
+ if (opts.market !== "perpetual") {
2291
+ const s = new OrderbookStream(() => void 0);
2292
+ queueMicrotask(
2293
+ () => s.emit(
2294
+ "error",
2295
+ new Error(
2296
+ "hyperliquid: only perpetual markets are supported (spot uses @N-indexed coins)"
2297
+ )
2298
+ )
2299
+ );
2300
+ return s;
2301
+ }
2302
+ const coin = toHyperliquidSymbol(opts.symbol);
2303
+ const merger = new BookMerger();
2304
+ let ws = null;
2305
+ const stream = new OrderbookStream(() => ws?.close());
2306
+ const subscribePayload = JSON.stringify({
2307
+ method: "subscribe",
2308
+ subscription: { type: "l2Book", coin }
2309
+ });
2310
+ const onMessage = (raw) => {
2311
+ if (typeof raw !== "string") return;
2312
+ let msg;
2313
+ try {
2314
+ msg = JSON.parse(raw);
2315
+ } catch {
2316
+ return;
2317
+ }
2318
+ if (msg.channel === "pong" || msg.channel === "subscriptionResponse") {
2319
+ return;
2320
+ }
2321
+ if (msg.channel !== "l2Book" || !msg.data) return;
2322
+ const book = msg.data;
2323
+ if (book.coin !== coin) return;
2324
+ const event = {
2325
+ kind: "snapshot",
2326
+ bids: book.levels[0].map((l) => ({
2327
+ price: Number(l.px),
2328
+ size: Number(l.sz)
2329
+ })),
2330
+ asks: book.levels[1].map((l) => ({
2331
+ price: Number(l.px),
2332
+ size: Number(l.sz)
2333
+ })),
2334
+ sequence: book.time,
2335
+ timestamp: book.time
2336
+ };
2337
+ const r = merger.apply(event);
2338
+ if (!r.ok) return;
2339
+ const out = {
2340
+ exchange: "hyperliquid",
2341
+ symbol: fromHyperliquidSymbol(coin),
2342
+ market: "perpetual",
2343
+ bids: r.bids,
2344
+ asks: r.asks,
2345
+ timestamp: r.timestamp,
2346
+ sequence: r.sequence
2347
+ };
2348
+ stream.emit("update", out);
2349
+ };
2350
+ ws = new WSClient({
2351
+ url: WS_URL,
2352
+ onOpen: (sock) => {
2353
+ merger.reset();
2354
+ sock.send(subscribePayload);
2355
+ },
2356
+ onMessage,
2357
+ pingIntervalMs: 5e4,
2358
+ pingPayload: () => JSON.stringify({ method: "ping" })
2359
+ });
2360
+ ws.on("open", () => stream.emit("connected"));
2361
+ ws.on("close", (r) => stream.emit("disconnected", r));
2362
+ ws.on("reconnecting", (a, w) => stream.emit("reconnecting", a, w));
2363
+ ws.on("error", (e) => stream.emit("error", e));
2364
+ ws.connect().catch((e) => stream.emit("error", e));
2365
+ return stream;
2366
+ }
2367
+
2368
+ // src/exchanges/hyperliquid/index.ts
2369
+ var HyperliquidClient = class {
2370
+ exchange = "hyperliquid";
2371
+ transformUrl;
2372
+ timeoutMs;
2373
+ constructor(opts = {}) {
2374
+ this.transformUrl = opts.transformUrl;
2375
+ this.timeoutMs = opts.timeoutMs;
2376
+ }
2377
+ fetchOrderbook(symbol, opts = {}) {
2378
+ const { pair, market } = parseSymbol(symbol);
2379
+ return fetchHyperliquidOrderbook({
2380
+ symbol: pair,
2381
+ market,
2382
+ depth: opts.depth,
2383
+ timeoutMs: this.timeoutMs,
2384
+ transformUrl: this.transformUrl
2385
+ });
2386
+ }
2387
+ streamOrderbook(symbol, opts = {}) {
2388
+ const { pair, market } = parseSymbol(symbol);
2389
+ return streamHyperliquidOrderbook({
2390
+ symbol: pair,
2391
+ market,
2392
+ depth: opts.depth,
2393
+ transformUrl: this.transformUrl,
2394
+ timeoutMs: this.timeoutMs
2395
+ });
2396
+ }
2397
+ };
2398
+
2399
+ // src/exchanges/kucoin/symbols.ts
2400
+ var FUTURES_QUOTES = ["USDT", "USDC", "USD"];
2401
+ function toKucoinSpotSymbol(symbol) {
2402
+ const [base, quote] = symbol.split("/");
2403
+ if (!base || !quote) {
2404
+ throw new Error(`invalid symbol "${symbol}" \u2014 expected "BASE/QUOTE"`);
2405
+ }
2406
+ return `${base.toUpperCase()}-${quote.toUpperCase()}`;
2407
+ }
2408
+ function fromKucoinSpotSymbol(s) {
2409
+ return s.toUpperCase().replace("-", "/");
2410
+ }
2411
+ function toKucoinFuturesSymbol(symbol) {
2412
+ const [base, quote] = symbol.split("/");
2413
+ if (!base || !quote) {
2414
+ throw new Error(`invalid symbol "${symbol}" \u2014 expected "BASE/QUOTE"`);
2415
+ }
2416
+ const b = base.toUpperCase() === "BTC" ? "XBT" : base.toUpperCase();
2417
+ return `${b}${quote.toUpperCase()}M`;
2418
+ }
2419
+ function fromKucoinFuturesSymbol(s) {
2420
+ const upper = s.toUpperCase();
2421
+ const stripped = upper.endsWith("M") ? upper.slice(0, -1) : upper;
2422
+ for (const q of FUTURES_QUOTES) {
2423
+ if (stripped.endsWith(q) && stripped.length > q.length) {
2424
+ let base = stripped.slice(0, -q.length);
2425
+ if (base === "XBT") base = "BTC";
2426
+ return `${base}/${q}`;
2427
+ }
2428
+ }
2429
+ return upper;
2430
+ }
2431
+
2432
+ // src/exchanges/kucoin/rest.ts
2433
+ var BASE_SPOT2 = "https://api.kucoin.com";
2434
+ var BASE_FUTURES = "https://api-futures.kucoin.com";
2435
+ async function fetchKucoinBullet(opts) {
2436
+ const base = opts.market === "spot" ? BASE_SPOT2 : BASE_FUTURES;
2437
+ const res = await httpJson({
2438
+ url: `${base}/api/v1/bullet-public`,
2439
+ method: "POST",
2440
+ timeoutMs: opts.timeoutMs,
2441
+ transformUrl: opts.transformUrl
2442
+ });
2443
+ if (res.code !== "200000") {
2444
+ throw new ExchangeError("kucoin", `bullet failed: ${res.code}`);
2445
+ }
2446
+ const server = res.data.instanceServers[0];
2447
+ if (!server) {
2448
+ throw new ExchangeError("kucoin", "bullet returned no instance servers");
2449
+ }
2450
+ return {
2451
+ endpoint: server.endpoint,
2452
+ token: res.data.token,
2453
+ pingInterval: server.pingInterval,
2454
+ pingTimeout: server.pingTimeout
2455
+ };
2456
+ }
2457
+ async function fetchKucoinOrderbook(opts) {
2458
+ const tier = (opts.depth ?? 100) <= 20 ? 20 : 100;
2459
+ if (opts.market === "spot") {
2460
+ const symbol2 = toKucoinSpotSymbol(opts.symbol);
2461
+ const url2 = `${BASE_SPOT2}/api/v1/market/orderbook/level2_${tier}?symbol=${symbol2}`;
2462
+ const res2 = await httpJson({
2463
+ url: url2,
2464
+ timeoutMs: opts.timeoutMs,
2465
+ transformUrl: opts.transformUrl
2466
+ });
2467
+ if (res2.code !== "200000") {
2468
+ throw new ExchangeError("kucoin", `orderbook failed: ${res2.code}`);
2469
+ }
2470
+ return {
2471
+ exchange: "kucoin",
2472
+ symbol: fromKucoinSpotSymbol(symbol2),
2473
+ market: "spot",
2474
+ bids: parseLevels(res2.data.bids),
2475
+ asks: parseLevels(res2.data.asks),
2476
+ timestamp: res2.data.time,
2477
+ sequence: Number(res2.data.sequence)
2478
+ };
2479
+ }
2480
+ const symbol = toKucoinFuturesSymbol(opts.symbol);
2481
+ const url = `${BASE_FUTURES}/api/v1/level2/depth${tier}?symbol=${symbol}`;
2482
+ const res = await httpJson({
2483
+ url,
2484
+ timeoutMs: opts.timeoutMs,
2485
+ transformUrl: opts.transformUrl
2486
+ });
2487
+ if (res.code !== "200000") {
2488
+ throw new ExchangeError("kucoin", `orderbook failed: ${res.code}`);
2489
+ }
2490
+ return {
2491
+ exchange: "kucoin",
2492
+ symbol: fromKucoinFuturesSymbol(symbol),
2493
+ market: "perpetual",
2494
+ bids: parseLevels(res.data.bids),
2495
+ asks: parseLevels(res.data.asks),
2496
+ timestamp: res.data.ts,
2497
+ sequence: res.data.sequence
2498
+ };
2499
+ }
2500
+ function parseLevels(rows) {
2501
+ return rows.map(([p, s]) => ({ price: Number(p), size: Number(s) }));
2502
+ }
2503
+
2504
+ // src/exchanges/kucoin/ws.ts
2505
+ function streamKucoinOrderbook(opts) {
2506
+ const isSpot = opts.market === "spot";
2507
+ const symbol = isSpot ? toKucoinSpotSymbol(opts.symbol) : toKucoinFuturesSymbol(opts.symbol);
2508
+ const level = (opts.depth ?? 50) <= 5 ? 5 : 50;
2509
+ const topic = isSpot ? `/spotMarket/level2Depth${level}:${symbol}` : `/contractMarket/level2Depth${level}:${symbol}`;
2510
+ const merger = new BookMerger();
2511
+ let ws = null;
2512
+ const stream = new OrderbookStream(() => ws?.close());
2513
+ const urlProvider = async () => {
2514
+ const bullet = await fetchKucoinBullet({
2515
+ market: opts.market,
2516
+ timeoutMs: opts.timeoutMs,
2517
+ transformUrl: opts.transformUrl
2518
+ });
2519
+ const connectId = `xapy-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
2520
+ return `${bullet.endpoint}?token=${encodeURIComponent(bullet.token)}&connectId=${connectId}`;
2521
+ };
2522
+ const subscribePayload = () => JSON.stringify({
2523
+ id: `sub-${Date.now()}`,
2524
+ type: "subscribe",
2525
+ topic,
2526
+ privateChannel: false,
2527
+ response: true
2528
+ });
2529
+ const pingPayload = () => JSON.stringify({ id: `${Date.now()}`, type: "ping" });
2530
+ const handleDepth = (d) => {
2531
+ const ts = d.timestamp ?? d.ts ?? Date.now();
2532
+ const event = {
2533
+ kind: "snapshot",
2534
+ bids: (d.bids ?? []).map(([p, s]) => ({
2535
+ price: Number(p),
2536
+ size: Number(s)
2537
+ })),
2538
+ asks: (d.asks ?? []).map(([p, s]) => ({
2539
+ price: Number(p),
2540
+ size: Number(s)
2541
+ })),
2542
+ sequence: ts,
2543
+ timestamp: ts
2544
+ };
2545
+ const r = merger.apply(event);
2546
+ if (!r.ok) return;
2547
+ const book = {
2548
+ exchange: "kucoin",
2549
+ symbol: isSpot ? fromKucoinSpotSymbol(symbol) : fromKucoinFuturesSymbol(symbol),
2550
+ market: opts.market,
2551
+ bids: r.bids,
2552
+ asks: r.asks,
2553
+ timestamp: r.timestamp,
2554
+ sequence: r.sequence
2555
+ };
2556
+ stream.emit("update", book);
2557
+ };
2558
+ const onMessage = (raw) => {
2559
+ if (typeof raw !== "string") return;
2560
+ let msg;
2561
+ try {
2562
+ msg = JSON.parse(raw);
2563
+ } catch {
2564
+ return;
2565
+ }
2566
+ if (msg.type === "welcome" || msg.type === "ack" || msg.type === "pong") {
2567
+ return;
2568
+ }
2569
+ if (msg.type === "error") {
2570
+ stream.emit("error", new Error(`kucoin: ${JSON.stringify(msg)}`));
2571
+ return;
2572
+ }
2573
+ if (msg.type !== "message" || msg.topic !== topic || !msg.data) return;
2574
+ handleDepth(msg.data);
2575
+ };
2576
+ ws = new WSClient({
2577
+ url: urlProvider,
2578
+ onOpen: (sock) => {
2579
+ merger.reset();
2580
+ sock.send(subscribePayload());
2581
+ },
2582
+ onMessage,
2583
+ // KuCoin's documented bullet `pingInterval` is 18000ms — stay under it.
2584
+ pingIntervalMs: 15e3,
2585
+ pingPayload
2586
+ });
2587
+ ws.on("open", () => stream.emit("connected"));
2588
+ ws.on("close", (r) => stream.emit("disconnected", r));
2589
+ ws.on("reconnecting", (a, w) => stream.emit("reconnecting", a, w));
2590
+ ws.on("error", (e) => stream.emit("error", e));
2591
+ ws.connect().catch((e) => stream.emit("error", e));
2592
+ return stream;
2593
+ }
2594
+
2595
+ // src/exchanges/kucoin/index.ts
2596
+ var KucoinClient = class {
2597
+ exchange = "kucoin";
2598
+ transformUrl;
2599
+ timeoutMs;
2600
+ constructor(opts = {}) {
2601
+ this.transformUrl = opts.transformUrl;
2602
+ this.timeoutMs = opts.timeoutMs;
2603
+ }
2604
+ fetchOrderbook(symbol, opts = {}) {
2605
+ const { pair, market } = parseSymbol(symbol);
2606
+ return fetchKucoinOrderbook({
2607
+ symbol: pair,
2608
+ market,
2609
+ depth: opts.depth,
2610
+ timeoutMs: this.timeoutMs,
2611
+ transformUrl: this.transformUrl
2612
+ });
2613
+ }
2614
+ streamOrderbook(symbol, opts = {}) {
2615
+ const { pair, market } = parseSymbol(symbol);
2616
+ return streamKucoinOrderbook({
2617
+ symbol: pair,
2618
+ market,
2619
+ depth: opts.depth,
2620
+ transformUrl: this.transformUrl,
2621
+ timeoutMs: this.timeoutMs
2622
+ });
2623
+ }
2624
+ };
2625
+
1917
2626
  exports.Backoff = Backoff;
2627
+ exports.BinanceClient = BinanceClient;
1918
2628
  exports.BingxClient = BingxClient;
1919
2629
  exports.BitgetClient = BitgetClient;
1920
2630
  exports.BookMerger = BookMerger;
@@ -1923,6 +2633,8 @@ exports.CoinexClient = CoinexClient;
1923
2633
  exports.ExchangeError = ExchangeError;
1924
2634
  exports.GateClient = GateClient;
1925
2635
  exports.HuobiClient = HuobiClient;
2636
+ exports.HyperliquidClient = HyperliquidClient;
2637
+ exports.KucoinClient = KucoinClient;
1926
2638
  exports.OkxClient = OkxClient;
1927
2639
  exports.OrderbookStream = OrderbookStream;
1928
2640
  exports.RateLimitError = RateLimitError;