@xapy/orderbook 0.1.2 → 0.1.4

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
@@ -847,9 +847,131 @@ function parseObjectLevels(rows) {
847
847
  var WS_SPOT2 = "wss://api.gateio.ws/ws/v4/";
848
848
  var WS_FUTURES = "wss://fx-ws.gateio.ws/v4/ws/usdt";
849
849
  function streamGateOrderbook(opts) {
850
- const isFutures = opts.market === "perpetual";
850
+ return opts.market === "perpetual" ? streamGateFuturesObu(opts) : streamGateSpot(opts);
851
+ }
852
+ function streamGateFuturesObu(opts) {
853
+ const gateSymbol = toGateSymbol(opts.symbol);
854
+ const level = snapFuturesLevel(opts.depth ?? 50);
855
+ const streamName = `ob.${gateSymbol}.${level}`;
856
+ const merger = new BookMerger();
857
+ let lastU = -1;
858
+ let ws = null;
859
+ const stream = new OrderbookStream(() => ws?.close());
860
+ const wsMsg = (event) => JSON.stringify({
861
+ time: Math.floor(Date.now() / 1e3),
862
+ channel: "futures.obu",
863
+ event,
864
+ payload: [streamName]
865
+ });
866
+ const pingMsg = () => JSON.stringify({
867
+ time: Math.floor(Date.now() / 1e3),
868
+ channel: "futures.ping"
869
+ });
870
+ const emitBook = (bids, asks, sequence, timestamp) => {
871
+ const book = {
872
+ exchange: "gate",
873
+ symbol: fromGateSymbol(gateSymbol),
874
+ market: "perpetual",
875
+ bids,
876
+ asks,
877
+ timestamp,
878
+ sequence
879
+ };
880
+ stream.emit("update", book);
881
+ };
882
+ const resubscribe = () => {
883
+ if (!ws) return;
884
+ merger.reset();
885
+ lastU = -1;
886
+ try {
887
+ ws.send(wsMsg("unsubscribe"));
888
+ ws.send(wsMsg("subscribe"));
889
+ } catch (err) {
890
+ stream.emit("error", err);
891
+ }
892
+ };
893
+ const processFrame = (frame) => {
894
+ const bids = parseObuLevels(frame.b);
895
+ const asks = parseObuLevels(frame.a);
896
+ if (frame.full) {
897
+ const r2 = merger.apply({
898
+ kind: "snapshot",
899
+ bids,
900
+ asks,
901
+ sequence: frame.u,
902
+ timestamp: frame.t
903
+ });
904
+ lastU = frame.u;
905
+ if (r2.ok) emitBook(r2.bids, r2.asks, r2.sequence, r2.timestamp);
906
+ return;
907
+ }
908
+ if (!merger.isReady() || lastU < 0 || frame.U === void 0) return;
909
+ if (frame.u <= lastU) return;
910
+ if (frame.U !== lastU + 1) {
911
+ resubscribe();
912
+ return;
913
+ }
914
+ const r = merger.apply({
915
+ kind: "delta",
916
+ bids,
917
+ asks,
918
+ sequence: frame.u,
919
+ prevSequence: lastU,
920
+ timestamp: frame.t
921
+ });
922
+ if (r.ok) {
923
+ lastU = frame.u;
924
+ emitBook(r.bids, r.asks, r.sequence, r.timestamp);
925
+ } else if (r.reason === "gap") {
926
+ resubscribe();
927
+ }
928
+ };
929
+ const onMessage = (raw) => {
930
+ if (typeof raw !== "string") return;
931
+ let msg;
932
+ try {
933
+ msg = JSON.parse(raw);
934
+ } catch {
935
+ return;
936
+ }
937
+ if (msg.error) {
938
+ stream.emit("error", new Error(`gate: ${msg.error.message}`));
939
+ return;
940
+ }
941
+ if (msg.channel !== "futures.obu") return;
942
+ if (msg.event === "subscribe" || msg.event === "unsubscribe") return;
943
+ if (msg.event !== "update" || !msg.result) return;
944
+ processFrame(msg.result);
945
+ };
946
+ ws = new WSClient({
947
+ url: WS_FUTURES,
948
+ onOpen: (sock) => {
949
+ merger.reset();
950
+ lastU = -1;
951
+ sock.send(wsMsg("subscribe"));
952
+ },
953
+ onMessage,
954
+ pingIntervalMs: 15e3,
955
+ pingPayload: pingMsg
956
+ });
957
+ ws.on("open", () => stream.emit("connected"));
958
+ ws.on("close", (r) => stream.emit("disconnected", r));
959
+ ws.on("reconnecting", (a, w) => stream.emit("reconnecting", a, w));
960
+ ws.on("error", (e) => stream.emit("error", e));
961
+ ws.connect().catch((e) => stream.emit("error", e));
962
+ return stream;
963
+ }
964
+ function snapFuturesLevel(depth) {
965
+ return depth <= 50 ? 50 : 400;
966
+ }
967
+ function parseObuLevels(rows) {
968
+ if (!rows) return [];
969
+ const out = [];
970
+ for (const [p, s] of rows) out.push({ price: Number(p), size: Number(s) });
971
+ return out;
972
+ }
973
+ function streamGateSpot(opts) {
851
974
  const gateSymbol = toGateSymbol(opts.symbol);
852
- const channel = isFutures ? "futures.order_book_update" : "spot.order_book_update";
853
975
  const interval = `${opts.intervalMs ?? 100}ms`;
854
976
  const depth = String(opts.depth ?? 100);
855
977
  const merger = new BookMerger();
@@ -861,22 +983,21 @@ function streamGateOrderbook(opts) {
861
983
  let recovering = false;
862
984
  let ws = null;
863
985
  const stream = new OrderbookStream(() => ws?.close());
864
- const payload = isFutures ? [gateSymbol, interval, depth] : [gateSymbol, depth, interval];
865
986
  const subscribeMsg = () => JSON.stringify({
866
987
  time: Math.floor(Date.now() / 1e3),
867
- channel,
988
+ channel: "spot.order_book_update",
868
989
  event: "subscribe",
869
- payload
990
+ payload: [gateSymbol, depth, interval]
870
991
  });
871
992
  const pingMsg = () => JSON.stringify({
872
993
  time: Math.floor(Date.now() / 1e3),
873
- channel: isFutures ? "futures.ping" : "spot.ping"
994
+ channel: "spot.ping"
874
995
  });
875
996
  const emitBook = (bids, asks, sequence, timestamp) => {
876
997
  const book = {
877
998
  exchange: "gate",
878
999
  symbol: fromGateSymbol(gateSymbol),
879
- market: opts.market,
1000
+ market: "spot",
880
1001
  bids,
881
1002
  asks,
882
1003
  timestamp,
@@ -889,7 +1010,7 @@ function streamGateOrderbook(opts) {
889
1010
  snapshotInFlight = true;
890
1011
  fetchGateOrderbook({
891
1012
  symbol: opts.symbol,
892
- market: opts.market,
1013
+ market: "spot",
893
1014
  depth: opts.depth ?? 100,
894
1015
  timeoutMs: opts.timeoutMs,
895
1016
  transformUrl: opts.transformUrl
@@ -965,9 +1086,16 @@ function streamGateOrderbook(opts) {
965
1086
  stream.emit("error", new Error(`gate: ${msg.error.message}`));
966
1087
  return;
967
1088
  }
968
- if (msg.channel !== channel || !msg.result) return;
1089
+ if (msg.channel !== "spot.order_book_update" || !msg.result) return;
969
1090
  if (msg.event !== "update") return;
970
- const ev = normalize(msg.result, isFutures);
1091
+ const r = msg.result;
1092
+ const ev = {
1093
+ U: r.U,
1094
+ u: r.u,
1095
+ t: r.t,
1096
+ bids: r.b.map(([p, s]) => ({ price: Number(p), size: Number(s) })),
1097
+ asks: r.a.map(([p, s]) => ({ price: Number(p), size: Number(s) }))
1098
+ };
971
1099
  if (!snapshotDone) {
972
1100
  buffered.push(ev);
973
1101
  if (!snapshotInFlight) triggerSnapshot();
@@ -976,7 +1104,7 @@ function streamGateOrderbook(opts) {
976
1104
  processDelta(ev);
977
1105
  };
978
1106
  ws = new WSClient({
979
- url: isFutures ? WS_FUTURES : WS_SPOT2,
1107
+ url: WS_SPOT2,
980
1108
  onOpen: (sock) => {
981
1109
  merger.reset();
982
1110
  snapshotSequence = -1;
@@ -996,26 +1124,6 @@ function streamGateOrderbook(opts) {
996
1124
  ws.connect().catch((e) => stream.emit("error", e));
997
1125
  return stream;
998
1126
  }
999
- function normalize(ev, isFutures) {
1000
- if (isFutures) {
1001
- const f = ev;
1002
- return {
1003
- U: f.U,
1004
- u: f.u,
1005
- t: f.t,
1006
- bids: f.b.map((row) => ({ price: Number(row.p), size: Number(row.s) })),
1007
- asks: f.a.map((row) => ({ price: Number(row.p), size: Number(row.s) }))
1008
- };
1009
- }
1010
- const s = ev;
1011
- return {
1012
- U: s.U,
1013
- u: s.u,
1014
- t: s.t,
1015
- bids: s.b.map(([p, sz]) => ({ price: Number(p), size: Number(sz) })),
1016
- asks: s.a.map(([p, sz]) => ({ price: Number(p), size: Number(sz) }))
1017
- };
1018
- }
1019
1127
 
1020
1128
  // src/exchanges/gate/index.ts
1021
1129
  var GateClient = class {
@@ -1123,29 +1231,50 @@ function streamBingxOrderbook(opts) {
1123
1231
  const wsSymbol = opts.symbol.toUpperCase().replace("/", "-");
1124
1232
  const level = opts.depth ?? 20;
1125
1233
  const interval = opts.interval ?? "500ms";
1126
- const dataType = `${wsSymbol}@depth${level}@${interval}`;
1234
+ const depthDataType = `${wsSymbol}@depth${level}@${interval}`;
1235
+ const tickerDataType = `${wsSymbol}@bookTicker`;
1127
1236
  const merger = new BookMerger();
1128
1237
  let ws = null;
1129
1238
  const stream = new OrderbookStream(() => ws?.close());
1130
- const subscribePayload = JSON.stringify({
1131
- id: `sub-${Date.now()}`,
1239
+ const subId = Date.now();
1240
+ const subscribeDepth = JSON.stringify({
1241
+ id: `sub-depth-${subId}`,
1132
1242
  reqType: "sub",
1133
- dataType
1243
+ dataType: depthDataType
1134
1244
  });
1135
- const handlePayload = (text) => {
1136
- if (text === "Ping" || text === "ping") {
1137
- ws?.send(text === "Ping" ? "Pong" : "pong");
1138
- return;
1139
- }
1140
- let msg;
1141
- try {
1142
- msg = JSON.parse(text);
1143
- } catch {
1144
- return;
1245
+ const subscribeTicker = JSON.stringify({
1246
+ id: `sub-ticker-${subId}`,
1247
+ reqType: "sub",
1248
+ dataType: tickerDataType
1249
+ });
1250
+ let lastMerged = null;
1251
+ let lastTicker = null;
1252
+ const emitOverlayed = () => {
1253
+ if (!lastMerged) return;
1254
+ let bids = lastMerged.bids;
1255
+ let asks = lastMerged.asks;
1256
+ let timestamp = lastMerged.timestamp;
1257
+ let sequence = lastMerged.sequence;
1258
+ if (lastTicker) {
1259
+ bids = overlayBid(bids, lastTicker.bid);
1260
+ asks = overlayAsk(asks, lastTicker.ask);
1261
+ if (lastTicker.ts > timestamp) {
1262
+ timestamp = lastTicker.ts;
1263
+ sequence = Math.max(sequence, lastTicker.ts);
1264
+ }
1145
1265
  }
1146
- if (msg.id && !msg.data) return;
1147
- if (msg.dataType !== dataType || !msg.data) return;
1148
- const d = msg.data;
1266
+ const book = {
1267
+ exchange: "bingx",
1268
+ symbol: opts.symbol,
1269
+ market: opts.market,
1270
+ bids,
1271
+ asks,
1272
+ timestamp,
1273
+ sequence
1274
+ };
1275
+ stream.emit("update", book);
1276
+ };
1277
+ const handleDepth = (d) => {
1149
1278
  const ts = d.T ?? d.ts ?? Date.now();
1150
1279
  const event = {
1151
1280
  kind: "snapshot",
@@ -1156,16 +1285,45 @@ function streamBingxOrderbook(opts) {
1156
1285
  };
1157
1286
  const r = merger.apply(event);
1158
1287
  if (!r.ok) return;
1159
- const book = {
1160
- exchange: "bingx",
1161
- symbol: opts.symbol,
1162
- market: opts.market,
1288
+ lastMerged = {
1163
1289
  bids: r.bids,
1164
1290
  asks: r.asks,
1165
1291
  timestamp: r.timestamp,
1166
1292
  sequence: r.sequence
1167
1293
  };
1168
- stream.emit("update", book);
1294
+ emitOverlayed();
1295
+ };
1296
+ const handleTicker = (t) => {
1297
+ const bidPrice = Number(t.b);
1298
+ const askPrice = Number(t.a);
1299
+ const bidSize = Number(t.B);
1300
+ const askSize = Number(t.A);
1301
+ if (!Number.isFinite(bidPrice) || !Number.isFinite(askPrice)) return;
1302
+ lastTicker = {
1303
+ bid: { price: bidPrice, size: bidSize },
1304
+ ask: { price: askPrice, size: askSize },
1305
+ ts: t.T ?? t.E ?? Date.now()
1306
+ };
1307
+ emitOverlayed();
1308
+ };
1309
+ const handlePayload = (text) => {
1310
+ if (text === "Ping" || text === "ping") {
1311
+ ws?.send(text === "Ping" ? "Pong" : "pong");
1312
+ return;
1313
+ }
1314
+ let msg;
1315
+ try {
1316
+ msg = JSON.parse(text);
1317
+ } catch {
1318
+ return;
1319
+ }
1320
+ if (msg.id && !msg.data) return;
1321
+ if (!msg.dataType || !msg.data) return;
1322
+ if (msg.dataType === depthDataType) {
1323
+ handleDepth(msg.data);
1324
+ } else if (msg.dataType === tickerDataType) {
1325
+ handleTicker(msg.data);
1326
+ }
1169
1327
  };
1170
1328
  const onMessage = async (raw) => {
1171
1329
  if (typeof raw === "string") {
@@ -1179,7 +1337,10 @@ function streamBingxOrderbook(opts) {
1179
1337
  url: isSpot ? WS_SPOT3 : WS_SWAP,
1180
1338
  onOpen: (sock) => {
1181
1339
  merger.reset();
1182
- sock.send(subscribePayload);
1340
+ lastMerged = null;
1341
+ lastTicker = null;
1342
+ sock.send(subscribeDepth);
1343
+ sock.send(subscribeTicker);
1183
1344
  },
1184
1345
  onMessage
1185
1346
  // bingx server is the one that sends Ping frames; we don't need client pings.
@@ -1191,6 +1352,32 @@ function streamBingxOrderbook(opts) {
1191
1352
  ws.connect().catch((e) => stream.emit("error", e));
1192
1353
  return stream;
1193
1354
  }
1355
+ function overlayAsk(asks, top) {
1356
+ if (!(top.size > 0)) return asks;
1357
+ let i = 0;
1358
+ for (; i < asks.length; i++) {
1359
+ const level = asks[i];
1360
+ if (!level || level.price >= top.price) break;
1361
+ }
1362
+ const at = asks[i];
1363
+ if (at && at.price === top.price) {
1364
+ return [{ price: top.price, size: top.size }, ...asks.slice(i + 1)];
1365
+ }
1366
+ return [{ price: top.price, size: top.size }, ...asks.slice(i)];
1367
+ }
1368
+ function overlayBid(bids, top) {
1369
+ if (!(top.size > 0)) return bids;
1370
+ let i = 0;
1371
+ for (; i < bids.length; i++) {
1372
+ const level = bids[i];
1373
+ if (!level || level.price <= top.price) break;
1374
+ }
1375
+ const at = bids[i];
1376
+ if (at && at.price === top.price) {
1377
+ return [{ price: top.price, size: top.size }, ...bids.slice(i + 1)];
1378
+ }
1379
+ return [{ price: top.price, size: top.size }, ...bids.slice(i)];
1380
+ }
1194
1381
 
1195
1382
  // src/exchanges/bingx/index.ts
1196
1383
  var BingxClient = class {