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