@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/exchanges/bingx/index.cjs +102 -23
- package/dist/exchanges/bingx/index.cjs.map +1 -1
- package/dist/exchanges/bingx/index.d.cts +9 -2
- package/dist/exchanges/bingx/index.d.ts +9 -2
- package/dist/exchanges/bingx/index.js +102 -23
- package/dist/exchanges/bingx/index.js.map +1 -1
- package/dist/exchanges/gate/index.cjs +139 -31
- package/dist/exchanges/gate/index.cjs.map +1 -1
- package/dist/exchanges/gate/index.d.cts +7 -14
- package/dist/exchanges/gate/index.d.ts +7 -14
- package/dist/exchanges/gate/index.js +139 -31
- package/dist/exchanges/gate/index.js.map +1 -1
- package/dist/index.cjs +241 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +241 -54
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 !==
|
|
1089
|
+
if (msg.channel !== "spot.order_book_update" || !msg.result) return;
|
|
969
1090
|
if (msg.event !== "update") return;
|
|
970
|
-
const
|
|
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:
|
|
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
|
|
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
|
|
1131
|
-
|
|
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
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|