@xapy/orderbook 0.1.21 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1618,61 +1618,93 @@ async function fetchCoinexOrderbook(opts) {
1618
1618
  }
1619
1619
 
1620
1620
  // src/exchanges/coinex/ws.ts
1621
- var WS_SPOT5 = "wss://socket.coinex.com/";
1622
- var WS_FUTURES2 = "wss://perpetual.coinex.com/";
1621
+ var WS_SPOT5 = "wss://socket.coinex.com/v2/spot";
1622
+ var WS_FUTURES2 = "wss://socket.coinex.com/v2/futures";
1623
1623
  function streamCoinexOrderbook(opts) {
1624
1624
  const market = toCoinexSymbol(opts.symbol);
1625
1625
  const depth = opts.depth ?? 50;
1626
1626
  const merger = new BookMerger();
1627
- let seq = 0;
1628
1627
  let ws = null;
1629
1628
  const stream = new OrderbookStream(() => ws?.close());
1630
- const subscribePayload = JSON.stringify({
1629
+ const subscribeDepth = JSON.stringify({
1631
1630
  method: "depth.subscribe",
1632
- params: [market, depth, "0", true],
1631
+ params: {
1632
+ // [market, limit, interval, if_full] — `true` = every push is a full
1633
+ // snapshot; the merger revalidates monotonic ordering by updated_at.
1634
+ market_list: [[market, depth, "0", true]]
1635
+ },
1633
1636
  id: 1
1634
1637
  });
1635
- const pingPayload = () => JSON.stringify({ method: "server.ping", params: [], id: 11 });
1636
- const emit = (r) => {
1638
+ const subscribeBbo = JSON.stringify({
1639
+ method: "bbo.subscribe",
1640
+ params: { market_list: [market] },
1641
+ id: 2
1642
+ });
1643
+ let lastMerged = null;
1644
+ let lastTicker = null;
1645
+ const emitOverlayed = () => {
1646
+ if (!lastMerged) return;
1647
+ let bids = lastMerged.bids;
1648
+ let asks = lastMerged.asks;
1649
+ let timestamp = lastMerged.timestamp;
1650
+ let sequence = lastMerged.sequence;
1651
+ if (lastTicker) {
1652
+ bids = overlayBid2(bids, lastTicker.bid);
1653
+ asks = overlayAsk2(asks, lastTicker.ask);
1654
+ if (lastTicker.ts > timestamp) {
1655
+ timestamp = lastTicker.ts;
1656
+ sequence = Math.max(sequence, lastTicker.ts);
1657
+ }
1658
+ }
1637
1659
  const book = {
1638
1660
  exchange: "coinex",
1639
1661
  symbol: fromCoinexSymbol(market),
1640
1662
  market: opts.market,
1663
+ bids,
1664
+ asks,
1665
+ timestamp,
1666
+ sequence
1667
+ };
1668
+ stream.emit("update", book);
1669
+ };
1670
+ const handleDepth = (d) => {
1671
+ if (d.market !== market) return;
1672
+ const inner = d.depth;
1673
+ const r = merger.apply({
1674
+ kind: "snapshot",
1675
+ bids: inner.bids.map(([p, s]) => ({
1676
+ price: Number(p),
1677
+ size: Number(s)
1678
+ })),
1679
+ asks: inner.asks.map(([p, s]) => ({
1680
+ price: Number(p),
1681
+ size: Number(s)
1682
+ })),
1683
+ sequence: inner.updated_at,
1684
+ timestamp: inner.updated_at
1685
+ });
1686
+ if (!r.ok) return;
1687
+ lastMerged = {
1641
1688
  bids: r.bids,
1642
1689
  asks: r.asks,
1643
1690
  timestamp: r.timestamp,
1644
1691
  sequence: r.sequence
1645
1692
  };
1646
- stream.emit("update", book);
1693
+ emitOverlayed();
1647
1694
  };
1648
- const handleDepthPush = (push) => {
1649
- const [clean, payload, mkt] = push;
1650
- if (mkt !== market) return;
1651
- const bids = parseLevels(payload.bids);
1652
- const asks = parseLevels(payload.asks);
1653
- const event = clean ? {
1654
- kind: "snapshot",
1655
- bids,
1656
- asks,
1657
- sequence: ++seq,
1658
- timestamp: payload.time
1659
- } : {
1660
- kind: "delta",
1661
- bids,
1662
- asks,
1663
- sequence: ++seq,
1664
- timestamp: payload.time
1695
+ const handleBbo = (b) => {
1696
+ if (b.market !== market) return;
1697
+ const bidPrice = Number(b.best_bid_price);
1698
+ const askPrice = Number(b.best_ask_price);
1699
+ const bidSize = Number(b.best_bid_size);
1700
+ const askSize = Number(b.best_ask_size);
1701
+ if (!Number.isFinite(bidPrice) || !Number.isFinite(askPrice)) return;
1702
+ lastTicker = {
1703
+ bid: { price: bidPrice, size: bidSize },
1704
+ ask: { price: askPrice, size: askSize },
1705
+ ts: b.updated_at
1665
1706
  };
1666
- const r = merger.apply(event);
1667
- if (r.ok) {
1668
- emit(r);
1669
- return;
1670
- }
1671
- if (r.reason === "no-snapshot") return;
1672
- if (r.reason === "gap") {
1673
- merger.reset();
1674
- seq = 0;
1675
- }
1707
+ emitOverlayed();
1676
1708
  };
1677
1709
  const handlePayload = (text) => {
1678
1710
  let msg;
@@ -1682,31 +1714,32 @@ function streamCoinexOrderbook(opts) {
1682
1714
  return;
1683
1715
  }
1684
1716
  if (msg.id != null && !msg.method) return;
1685
- if (msg.error) {
1686
- stream.emit(
1687
- "error",
1688
- new Error(`coinex: ${msg.error.code} ${msg.error.message}`)
1689
- );
1690
- return;
1691
- }
1692
- if (msg.method === "depth.update" && Array.isArray(msg.params)) {
1693
- handleDepthPush(msg.params);
1717
+ if (!msg.data || !msg.method) return;
1718
+ if (msg.method === "depth.update") {
1719
+ handleDepth(msg.data);
1720
+ } else if (msg.method === "bbo.update") {
1721
+ handleBbo(msg.data);
1694
1722
  }
1695
1723
  };
1696
- const onMessage = (raw) => {
1697
- if (typeof raw !== "string") return;
1698
- handlePayload(raw);
1724
+ const onMessage = async (raw) => {
1725
+ if (typeof raw === "string") {
1726
+ handlePayload(raw);
1727
+ return;
1728
+ }
1729
+ const inflate = await getGzipInflate();
1730
+ handlePayload(inflate(raw));
1699
1731
  };
1700
1732
  ws = new WSClient({
1701
1733
  url: opts.market === "spot" ? WS_SPOT5 : WS_FUTURES2,
1702
1734
  onOpen: (sock) => {
1703
1735
  merger.reset();
1704
- seq = 0;
1705
- sock.send(subscribePayload);
1736
+ lastMerged = null;
1737
+ lastTicker = null;
1738
+ sock.send(subscribeDepth);
1739
+ sock.send(subscribeBbo);
1706
1740
  },
1707
- onMessage,
1708
- pingIntervalMs: 25e3,
1709
- pingPayload
1741
+ onMessage
1742
+ // Native WS ping/pong frames keep the connection alive — no app-level ping.
1710
1743
  });
1711
1744
  ws.on("open", () => stream.emit("connected"));
1712
1745
  ws.on("close", (r) => stream.emit("disconnected", r));
@@ -1715,11 +1748,31 @@ function streamCoinexOrderbook(opts) {
1715
1748
  ws.connect().catch((e) => stream.emit("error", e));
1716
1749
  return stream;
1717
1750
  }
1718
- function parseLevels(rows) {
1719
- if (!rows) return [];
1720
- const out = [];
1721
- for (const [p, s] of rows) out.push({ price: Number(p), size: Number(s) });
1722
- return out;
1751
+ function overlayAsk2(asks, top) {
1752
+ if (!(top.size > 0)) return asks;
1753
+ let i = 0;
1754
+ for (; i < asks.length; i++) {
1755
+ const level = asks[i];
1756
+ if (!level || level.price >= top.price) break;
1757
+ }
1758
+ const at = asks[i];
1759
+ if (at && at.price === top.price) {
1760
+ return [{ price: top.price, size: top.size }, ...asks.slice(i + 1)];
1761
+ }
1762
+ return [{ price: top.price, size: top.size }, ...asks.slice(i)];
1763
+ }
1764
+ function overlayBid2(bids, top) {
1765
+ if (!(top.size > 0)) return bids;
1766
+ let i = 0;
1767
+ for (; i < bids.length; i++) {
1768
+ const level = bids[i];
1769
+ if (!level || level.price <= top.price) break;
1770
+ }
1771
+ const at = bids[i];
1772
+ if (at && at.price === top.price) {
1773
+ return [{ price: top.price, size: top.size }, ...bids.slice(i + 1)];
1774
+ }
1775
+ return [{ price: top.price, size: top.size }, ...bids.slice(i)];
1723
1776
  }
1724
1777
 
1725
1778
  // src/exchanges/coinex/index.ts
@@ -2417,8 +2470,8 @@ async function fetchKucoinOrderbook(opts) {
2417
2470
  exchange: "kucoin",
2418
2471
  symbol: fromKucoinSpotSymbol(symbol2),
2419
2472
  market: "spot",
2420
- bids: parseLevels2(res2.data.bids),
2421
- asks: parseLevels2(res2.data.asks),
2473
+ bids: parseLevels(res2.data.bids),
2474
+ asks: parseLevels(res2.data.asks),
2422
2475
  timestamp: res2.data.time,
2423
2476
  sequence: Number(res2.data.sequence)
2424
2477
  };
@@ -2437,13 +2490,13 @@ async function fetchKucoinOrderbook(opts) {
2437
2490
  exchange: "kucoin",
2438
2491
  symbol: fromKucoinFuturesSymbol(symbol),
2439
2492
  market: "perpetual",
2440
- bids: parseLevels2(res.data.bids),
2441
- asks: parseLevels2(res.data.asks),
2493
+ bids: parseLevels(res.data.bids),
2494
+ asks: parseLevels(res.data.asks),
2442
2495
  timestamp: res.data.ts,
2443
2496
  sequence: res.data.sequence
2444
2497
  };
2445
2498
  }
2446
- function parseLevels2(rows) {
2499
+ function parseLevels(rows) {
2447
2500
  return rows.map(([p, s]) => ({ price: Number(p), size: Number(s) }));
2448
2501
  }
2449
2502
 
@@ -2611,13 +2664,13 @@ async function fetchBitmartOrderbook(opts) {
2611
2664
  exchange: "bitmart",
2612
2665
  symbol: fromBitmartSymbol(res.data.symbol),
2613
2666
  market: "perpetual",
2614
- bids: parseLevels3(res.data.bids),
2615
- asks: parseLevels3(res.data.asks),
2667
+ bids: parseLevels2(res.data.bids),
2668
+ asks: parseLevels2(res.data.asks),
2616
2669
  timestamp: res.data.timestamp,
2617
2670
  sequence: res.data.timestamp
2618
2671
  };
2619
2672
  }
2620
- function parseLevels3(rows) {
2673
+ function parseLevels2(rows) {
2621
2674
  const out = [];
2622
2675
  for (const row of rows) {
2623
2676
  out.push({ price: Number(row[0]), size: Number(row[1]) });
@@ -2650,8 +2703,8 @@ function streamBitmartOrderbook(opts) {
2650
2703
  if (frame.symbol !== symbol) return;
2651
2704
  const event = {
2652
2705
  kind: "snapshot",
2653
- bids: parseLevels4(frame.bids),
2654
- asks: parseLevels4(frame.asks),
2706
+ bids: parseLevels3(frame.bids),
2707
+ asks: parseLevels3(frame.asks),
2655
2708
  sequence: frame.ms_t,
2656
2709
  timestamp: frame.ms_t
2657
2710
  };
@@ -2716,7 +2769,7 @@ function snapLevel(depth) {
2716
2769
  if (depth === 20) return 20;
2717
2770
  return 50;
2718
2771
  }
2719
- function parseLevels4(rows) {
2772
+ function parseLevels3(rows) {
2720
2773
  if (!rows) return [];
2721
2774
  const out = [];
2722
2775
  for (const r of rows) out.push({ price: Number(r.price), size: Number(r.vol) });