@xapy/orderbook 0.1.9 → 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]) });
@@ -2633,7 +2686,9 @@ function streamBitmartOrderbook(opts) {
2633
2686
  );
2634
2687
  }
2635
2688
  const symbol = toBitmartSymbol(opts.symbol);
2636
- const channelArg = opts.interval ? `futures/depth:${symbol}@${opts.interval}` : `futures/depth:${symbol}`;
2689
+ const level = snapLevel(opts.depth);
2690
+ const interval = opts.interval ?? "200ms";
2691
+ const channelArg = `futures/depthAll${level}:${symbol}@${interval}`;
2637
2692
  const merger = new BookMerger();
2638
2693
  let ws = null;
2639
2694
  const stream = new OrderbookStream(() => ws?.close());
@@ -2646,10 +2701,10 @@ function streamBitmartOrderbook(opts) {
2646
2701
  if (frame.symbol !== symbol) return;
2647
2702
  const event = {
2648
2703
  kind: "snapshot",
2649
- bids: parseLevels4(frame.bids),
2650
- asks: parseLevels4(frame.asks),
2651
- sequence: frame.timestamp,
2652
- timestamp: frame.timestamp
2704
+ bids: parseLevels3(frame.bids),
2705
+ asks: parseLevels3(frame.asks),
2706
+ sequence: frame.ms_t,
2707
+ timestamp: frame.ms_t
2653
2708
  };
2654
2709
  const r = merger.apply(event);
2655
2710
  if (!r.ok) return;
@@ -2679,7 +2734,7 @@ function streamBitmartOrderbook(opts) {
2679
2734
  );
2680
2735
  return;
2681
2736
  }
2682
- if (msg.arg !== channelArg || !msg.data) return;
2737
+ if (msg.group !== channelArg || !msg.data) return;
2683
2738
  handleFrame(msg.data);
2684
2739
  };
2685
2740
  const onMessage = async (raw) => {
@@ -2707,12 +2762,15 @@ function streamBitmartOrderbook(opts) {
2707
2762
  ws.connect().catch((e) => stream.emit("error", e));
2708
2763
  return stream;
2709
2764
  }
2710
- function parseLevels4(rows) {
2765
+ function snapLevel(depth) {
2766
+ if (depth === 5) return 5;
2767
+ if (depth === 20) return 20;
2768
+ return 50;
2769
+ }
2770
+ function parseLevels3(rows) {
2711
2771
  if (!rows) return [];
2712
2772
  const out = [];
2713
- for (const row of rows) {
2714
- out.push({ price: Number(row[0]), size: Number(row[1]) });
2715
- }
2773
+ for (const r of rows) out.push({ price: Number(r.price), size: Number(r.vol) });
2716
2774
  return out;
2717
2775
  }
2718
2776