@blackcube/xgate-sdk 0.22.0 → 0.23.0

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
@@ -21,131 +21,6 @@ var __decorateClass = (decorators, target, key, kind) => {
21
21
  return result;
22
22
  };
23
23
 
24
- // src/enums/timeframe.enums.ts
25
- var Timeframe = /* @__PURE__ */ ((Timeframe2) => {
26
- Timeframe2["M1"] = "1m";
27
- Timeframe2["M5"] = "5m";
28
- Timeframe2["M15"] = "15m";
29
- Timeframe2["H1"] = "1h";
30
- Timeframe2["H4"] = "4h";
31
- Timeframe2["D"] = "1d";
32
- Timeframe2["W"] = "1w";
33
- return Timeframe2;
34
- })(Timeframe || {});
35
- var TIMEFRAME_MINUTES = {
36
- ["1m" /* M1 */]: 1,
37
- ["5m" /* M5 */]: 5,
38
- ["15m" /* M15 */]: 15,
39
- ["1h" /* H1 */]: 60,
40
- ["4h" /* H4 */]: 240,
41
- ["1d" /* D */]: 1440,
42
- ["1w" /* W */]: 10080
43
- };
44
- function timeframeMs(timeframe) {
45
- return TIMEFRAME_MINUTES[timeframe] * 6e4;
46
- }
47
-
48
- // src/helpers/decimal.ts
49
- function parse(value) {
50
- const trimmed = value.trim();
51
- if (/^-?\d+(\.\d+)?$/.test(trimmed) === false) {
52
- throw new Error(`D\xE9cimal illisible : "${value}".`);
53
- }
54
- const dot = trimmed.indexOf(".");
55
- if (dot < 0) {
56
- return { digits: BigInt(trimmed), scale: 0 };
57
- }
58
- return {
59
- digits: BigInt(trimmed.slice(0, dot) + trimmed.slice(dot + 1)),
60
- scale: trimmed.length - dot - 1
61
- };
62
- }
63
- function format(digits, scale) {
64
- const negative = digits < 0n;
65
- const plain = (negative === true ? -digits : digits).toString().padStart(scale + 1, "0");
66
- const sign = negative === true ? "-" : "";
67
- if (scale === 0) {
68
- return sign + plain;
69
- }
70
- const whole = plain.slice(0, plain.length - scale);
71
- const fraction = plain.slice(plain.length - scale).replace(/0+$/, "");
72
- return fraction === "" ? sign + whole : `${sign + whole}.${fraction}`;
73
- }
74
- function addDecimals(left, right) {
75
- const a = parse(left);
76
- const b = parse(right);
77
- const scale = Math.max(a.scale, b.scale);
78
- const digits = a.digits * 10n ** BigInt(scale - a.scale) + b.digits * 10n ** BigInt(scale - b.scale);
79
- return format(digits, scale);
80
- }
81
- function compareDecimals(left, right) {
82
- const a = parse(left);
83
- const b = parse(right);
84
- const scale = Math.max(a.scale, b.scale);
85
- const x = a.digits * 10n ** BigInt(scale - a.scale);
86
- const y = b.digits * 10n ** BigInt(scale - b.scale);
87
- if (x === y) {
88
- return 0;
89
- }
90
- return x > y ? 1 : -1;
91
- }
92
-
93
- // src/helpers/candle-aggregator.ts
94
- var MONDAY_OFFSET_MS = 4 * 24 * 60 * 60 * 1e3;
95
- function aggregate(source, target, now = Date.now()) {
96
- if (source.length === 0) {
97
- return [];
98
- }
99
- const sourceMs = timeframeMs(source[0]?.interval ?? "1m" /* M1 */);
100
- const targetMs = timeframeMs(target);
101
- if (targetMs <= sourceMs) {
102
- return [];
103
- }
104
- const offsetMs = target === "1w" /* W */ ? MONDAY_OFFSET_MS : 0;
105
- const expected = targetMs / sourceMs;
106
- const buckets = /* @__PURE__ */ new Map();
107
- const sorted = [...source].sort(
108
- (left, right) => left.openedAt.getTime() - right.openedAt.getTime()
109
- );
110
- for (const candle of sorted) {
111
- const start = Math.floor((candle.openedAt.getTime() - offsetMs) / targetMs) * targetMs + offsetMs;
112
- const bucket = buckets.get(start);
113
- if (bucket === void 0) {
114
- buckets.set(start, {
115
- sources: 1,
116
- candle: {
117
- ...candle,
118
- interval: target,
119
- openedAt: new Date(start),
120
- // Dernière milliseconde du créneau, comme les bougies servies par les venues.
121
- closedAt: new Date(start + targetMs - 1),
122
- derived: true
123
- }
124
- });
125
- } else {
126
- bucket.sources += 1;
127
- const built = bucket.candle;
128
- built.high = compareDecimals(candle.high, built.high) > 0 ? candle.high : built.high;
129
- built.low = compareDecimals(candle.low, built.low) < 0 ? candle.low : built.low;
130
- built.close = candle.close;
131
- built.volume = sum(built.volume, candle.volume);
132
- built.quoteVolume = sum(built.quoteVolume, candle.quoteVolume);
133
- built.takerBuyQuoteVolume = sum(built.takerBuyQuoteVolume, candle.takerBuyQuoteVolume);
134
- built.trades = built.trades === null || candle.trades === null ? null : built.trades + candle.trades;
135
- }
136
- }
137
- return [...buckets.keys()].sort((left, right) => left - right).map((start) => buckets.get(start)).filter((bucket) => bucket !== void 0).filter((bucket) => {
138
- const stillOpen = bucket.candle.openedAt.getTime() + targetMs > now;
139
- return bucket.sources >= expected || stillOpen === true;
140
- }).map((bucket) => bucket.candle);
141
- }
142
- function sum(left, right) {
143
- if (left === null || right === null) {
144
- return null;
145
- }
146
- return addDecimals(left, right);
147
- }
148
-
149
24
  // src/helpers/symbol-naming.ts
150
25
  var CONCAT_QUOTE_XEXS = /* @__PURE__ */ new Set(["aster", "binance", "bybit"]);
151
26
  var K_MULTIPLIER = /^k(?=[A-Z])/u;
@@ -221,15 +96,13 @@ function toCandle(wire, xex, interval, quote) {
221
96
  };
222
97
  }
223
98
 
224
- // src/helpers/candle-query.ts
225
- function toXexQuery(query) {
226
- return {
227
- name: query.symbol,
228
- interval: query.interval,
229
- startTime: query.startTime,
230
- endTime: query.endTime,
231
- limit: query.limit
232
- };
99
+ // src/helpers/ws-limits.ts
100
+ var WS_SUBSCRIPTION_LIMITS = {
101
+ aster: 200,
102
+ hyperliquid: 1e3
103
+ };
104
+ function subscriptionLimitOf(xex) {
105
+ return WS_SUBSCRIPTION_LIMITS[xex];
233
106
  }
234
107
 
235
108
  // src/enums/xgate-ex.enums.ts
@@ -377,6 +250,282 @@ function createPublicSpotXex(xex) {
377
250
  }
378
251
  }
379
252
 
253
+ // src/services/candles-stream.service.ts
254
+ var CandlesStreamService = class {
255
+ constructor(xex, kind = "perp") {
256
+ this.xex = xex;
257
+ this.kind = kind;
258
+ this.logger = new common.Logger(`CandlesStream:${xex}${kind === "spot" ? ":spot" : ""}`);
259
+ this.maxSubscriptions = subscriptionLimitOf(xex) ?? null;
260
+ }
261
+ xex;
262
+ kind;
263
+ logger;
264
+ source = null;
265
+ unsubscribes = [];
266
+ /** Le plafond de souscriptions documenté par la venue, ou `null` si personne ne l'a publié. */
267
+ maxSubscriptions;
268
+ /**
269
+ * Ouvre le flux sur une LISTE de marchés, sur une seule socket.
270
+ *
271
+ * **UNE ERREUR DE FLUX COUPE TOUT, PUIS LÈVE.** Elle n'arrive pas à la souscription : mesuré le
272
+ * 2026-08-07, la venue accepte un symbole inconnu sans broncher, puis ferme la connexion en le
273
+ * découvrant. Le SDK reconnecte, elle referme — cinq cycles en douze secondes, et sur une socket
274
+ * partagée **tous** les marchés valides cessent de recevoir, en silence.
275
+ *
276
+ * On refuse cette boucle : à la première erreur, les souscriptions sont coupées, l'incident est
277
+ * journalisé en `error`, et l'exception part. Un flux à moitié mort qui se tait coûte plus cher
278
+ * qu'un arrêt franc — on ne découvre le premier qu'en constatant l'absence de données.
279
+ *
280
+ * ⚠️ L'exception naît dans un rappel de socket : elle ne remonte pas à l'appelant de `start()`,
281
+ * elle sort en erreur non capturée. C'est délibéré — elle doit être impossible à ignorer.
282
+ *
283
+ * **binance et bybit ne remontent pas encore leurs erreurs** : leur client gère la fermeture en
284
+ * interne sans l'exposer. Sur ces deux venues, la boucle silencieuse reste possible.
285
+ */
286
+ start(symbolsXex, interval, onCandle) {
287
+ const ws = this.wire();
288
+ if ("onError" in ws) {
289
+ ws.onError = (error) => {
290
+ const raison = lisible(error);
291
+ this.logger.error(`${this.xex} : erreur de flux \u2014 ${raison}. Souscriptions coup\xE9es.`);
292
+ this.stop();
293
+ throw new Error(`stream(${this.xex}) : flux interrompu \u2014 ${raison}`);
294
+ };
295
+ }
296
+ for (const symbolXex of symbolsXex) {
297
+ this.unsubscribes.push(
298
+ ws.subscribeCandles({ name: symbolXex, interval }, (wire) => {
299
+ onCandle(toCandle(wire, this.xex, interval, this.quoteOf(symbolXex)));
300
+ })
301
+ );
302
+ }
303
+ this.logger.log(`${this.unsubscribes.length}/${symbolsXex.length} souscription(s) ouvertes`);
304
+ }
305
+ /** Coupe tout. Un désabonnement qui échoue ne doit pas empêcher les autres de se fermer. */
306
+ stop() {
307
+ for (const unsubscribe of this.unsubscribes.splice(0)) {
308
+ try {
309
+ unsubscribe();
310
+ } catch {
311
+ }
312
+ }
313
+ }
314
+ /** Le nombre de souscriptions ouvertes. La liste des désabonnements EST le compteur. */
315
+ subscriptionCount() {
316
+ return this.unsubscribes.length;
317
+ }
318
+ /**
319
+ * Le client temps réel, sur le bon marché.
320
+ *
321
+ * Les deux contrats ne signent pas `ws()` pareil, et c'est voulu : côté comptant le marché est
322
+ * **obligatoire** (`ws('spot')`), parce que les façades ouvrent du perpétuel par défaut — un
323
+ * `ws()` nu y diffuserait des prix de perpétuel à qui croit écouter le comptant.
324
+ */
325
+ wire() {
326
+ const source = this.instance();
327
+ return this.kind === "spot" ? source.ws("spot") : source.ws();
328
+ }
329
+ /** La façade de la venue, créée une seule fois : c'est elle qui porte la socket partagée. */
330
+ instance() {
331
+ if (this.source === null) {
332
+ if (this.kind === "spot" && servesSpot(this.xex) === false) {
333
+ throw new Error(`stream(${this.xex}) : cette venue ne sert pas de comptant.`);
334
+ }
335
+ this.source = this.kind === "spot" ? createPublicSpotXex(this.xex) : createPublicXex(this.xex);
336
+ }
337
+ return this.source;
338
+ }
339
+ /** Voir `CandlesService.quoteOf` : la cotation se déduit du symbole natif, faute d'être publiée. */
340
+ quoteOf(symbolXex) {
341
+ const parts = symbolXex.split("-");
342
+ const last = parts[parts.length - 1];
343
+ return parts.length > 1 && last !== void 0 ? last : "";
344
+ }
345
+ };
346
+ exports.CandlesStreamRegistry = class CandlesStreamRegistry {
347
+ streams = /* @__PURE__ */ new Map();
348
+ /** Le flux d'une venue, créé au premier appel puis réutilisé. */
349
+ of(xex, kind = "perp") {
350
+ const cle = `${xex}:${kind}`;
351
+ let stream = this.streams.get(cle);
352
+ if (stream === void 0) {
353
+ stream = new CandlesStreamService(xex, kind);
354
+ this.streams.set(cle, stream);
355
+ }
356
+ return stream;
357
+ }
358
+ /** Coupe tous les flux ouverts, toutes venues confondues. */
359
+ stopAll() {
360
+ for (const stream of this.streams.values()) {
361
+ stream.stop();
362
+ }
363
+ }
364
+ /** Ce qui est ouvert, par venue — pour surveiller sans avoir à tenir de compteur soi-même. */
365
+ counts() {
366
+ const total = {};
367
+ for (const [cle, stream] of this.streams) {
368
+ total[cle] = stream.subscriptionCount();
369
+ }
370
+ return total;
371
+ }
372
+ };
373
+ exports.CandlesStreamRegistry = __decorateClass([
374
+ common.Injectable()
375
+ ], exports.CandlesStreamRegistry);
376
+ function lisible(error) {
377
+ if (error instanceof Error) {
378
+ return error.message;
379
+ }
380
+ const event = error;
381
+ if (event !== null && typeof event === "object") {
382
+ const message = event.message ?? event.error?.message;
383
+ if (typeof message === "string" && message !== "") {
384
+ return message;
385
+ }
386
+ if (typeof event.type === "string" && event.type !== "") {
387
+ return `\xE9v\xE9nement \xAB ${event.type} \xBB`;
388
+ }
389
+ }
390
+ return String(error);
391
+ }
392
+
393
+ // src/enums/timeframe.enums.ts
394
+ var Timeframe = /* @__PURE__ */ ((Timeframe2) => {
395
+ Timeframe2["M1"] = "1m";
396
+ Timeframe2["M5"] = "5m";
397
+ Timeframe2["M15"] = "15m";
398
+ Timeframe2["H1"] = "1h";
399
+ Timeframe2["H4"] = "4h";
400
+ Timeframe2["D"] = "1d";
401
+ Timeframe2["W"] = "1w";
402
+ return Timeframe2;
403
+ })(Timeframe || {});
404
+ var TIMEFRAME_MINUTES = {
405
+ ["1m" /* M1 */]: 1,
406
+ ["5m" /* M5 */]: 5,
407
+ ["15m" /* M15 */]: 15,
408
+ ["1h" /* H1 */]: 60,
409
+ ["4h" /* H4 */]: 240,
410
+ ["1d" /* D */]: 1440,
411
+ ["1w" /* W */]: 10080
412
+ };
413
+ function timeframeMs(timeframe) {
414
+ return TIMEFRAME_MINUTES[timeframe] * 6e4;
415
+ }
416
+
417
+ // src/helpers/decimal.ts
418
+ function parse(value) {
419
+ const trimmed = value.trim();
420
+ if (/^-?\d+(\.\d+)?$/.test(trimmed) === false) {
421
+ throw new Error(`D\xE9cimal illisible : "${value}".`);
422
+ }
423
+ const dot = trimmed.indexOf(".");
424
+ if (dot < 0) {
425
+ return { digits: BigInt(trimmed), scale: 0 };
426
+ }
427
+ return {
428
+ digits: BigInt(trimmed.slice(0, dot) + trimmed.slice(dot + 1)),
429
+ scale: trimmed.length - dot - 1
430
+ };
431
+ }
432
+ function format(digits, scale) {
433
+ const negative = digits < 0n;
434
+ const plain = (negative === true ? -digits : digits).toString().padStart(scale + 1, "0");
435
+ const sign = negative === true ? "-" : "";
436
+ if (scale === 0) {
437
+ return sign + plain;
438
+ }
439
+ const whole = plain.slice(0, plain.length - scale);
440
+ const fraction = plain.slice(plain.length - scale).replace(/0+$/, "");
441
+ return fraction === "" ? sign + whole : `${sign + whole}.${fraction}`;
442
+ }
443
+ function addDecimals(left, right) {
444
+ const a = parse(left);
445
+ const b = parse(right);
446
+ const scale = Math.max(a.scale, b.scale);
447
+ const digits = a.digits * 10n ** BigInt(scale - a.scale) + b.digits * 10n ** BigInt(scale - b.scale);
448
+ return format(digits, scale);
449
+ }
450
+ function compareDecimals(left, right) {
451
+ const a = parse(left);
452
+ const b = parse(right);
453
+ const scale = Math.max(a.scale, b.scale);
454
+ const x = a.digits * 10n ** BigInt(scale - a.scale);
455
+ const y = b.digits * 10n ** BigInt(scale - b.scale);
456
+ if (x === y) {
457
+ return 0;
458
+ }
459
+ return x > y ? 1 : -1;
460
+ }
461
+
462
+ // src/helpers/candle-aggregator.ts
463
+ var MONDAY_OFFSET_MS = 4 * 24 * 60 * 60 * 1e3;
464
+ function aggregate(source, target, now = Date.now()) {
465
+ if (source.length === 0) {
466
+ return [];
467
+ }
468
+ const sourceMs = timeframeMs(source[0]?.interval ?? "1m" /* M1 */);
469
+ const targetMs = timeframeMs(target);
470
+ if (targetMs <= sourceMs) {
471
+ return [];
472
+ }
473
+ const offsetMs = target === "1w" /* W */ ? MONDAY_OFFSET_MS : 0;
474
+ const expected = targetMs / sourceMs;
475
+ const buckets = /* @__PURE__ */ new Map();
476
+ const sorted = [...source].sort(
477
+ (left, right) => left.openedAt.getTime() - right.openedAt.getTime()
478
+ );
479
+ for (const candle of sorted) {
480
+ const start = Math.floor((candle.openedAt.getTime() - offsetMs) / targetMs) * targetMs + offsetMs;
481
+ const bucket = buckets.get(start);
482
+ if (bucket === void 0) {
483
+ buckets.set(start, {
484
+ sources: 1,
485
+ candle: {
486
+ ...candle,
487
+ interval: target,
488
+ openedAt: new Date(start),
489
+ // Dernière milliseconde du créneau, comme les bougies servies par les venues.
490
+ closedAt: new Date(start + targetMs - 1),
491
+ derived: true
492
+ }
493
+ });
494
+ } else {
495
+ bucket.sources += 1;
496
+ const built = bucket.candle;
497
+ built.high = compareDecimals(candle.high, built.high) > 0 ? candle.high : built.high;
498
+ built.low = compareDecimals(candle.low, built.low) < 0 ? candle.low : built.low;
499
+ built.close = candle.close;
500
+ built.volume = sum(built.volume, candle.volume);
501
+ built.quoteVolume = sum(built.quoteVolume, candle.quoteVolume);
502
+ built.takerBuyQuoteVolume = sum(built.takerBuyQuoteVolume, candle.takerBuyQuoteVolume);
503
+ built.trades = built.trades === null || candle.trades === null ? null : built.trades + candle.trades;
504
+ }
505
+ }
506
+ return [...buckets.keys()].sort((left, right) => left - right).map((start) => buckets.get(start)).filter((bucket) => bucket !== void 0).filter((bucket) => {
507
+ const stillOpen = bucket.candle.openedAt.getTime() + targetMs > now;
508
+ return bucket.sources >= expected || stillOpen === true;
509
+ }).map((bucket) => bucket.candle);
510
+ }
511
+ function sum(left, right) {
512
+ if (left === null || right === null) {
513
+ return null;
514
+ }
515
+ return addDecimals(left, right);
516
+ }
517
+
518
+ // src/helpers/candle-query.ts
519
+ function toXexQuery(query) {
520
+ return {
521
+ name: query.symbol,
522
+ interval: query.interval,
523
+ startTime: query.startTime,
524
+ endTime: query.endTime,
525
+ limit: query.limit
526
+ };
527
+ }
528
+
380
529
  // src/helpers/xex-timeframes.ts
381
530
  var HYPERLIQUID_NATIVE = Object.values(Timeframe).filter(
382
531
  (interval) => interval !== "1w" /* W */
@@ -798,6 +947,133 @@ exports.SpotCatalogService = class SpotCatalogService {
798
947
  exports.SpotCatalogService = __decorateClass([
799
948
  common.Injectable()
800
949
  ], exports.SpotCatalogService);
950
+ exports.SpotPricesService = class SpotPricesService {
951
+ logger = new common.Logger(exports.SpotPricesService.name);
952
+ /** Les venues dont XGate sert les prix comptant. */
953
+ venues() {
954
+ return SPOT_XEXES;
955
+ }
956
+ /**
957
+ * Les prix comptant d'une ou plusieurs venues, en une seule liste.
958
+ *
959
+ * Même politique d'échec que partout : seule, une venue en échec fait échouer l'appel ; parmi
960
+ * d'autres, elle est journalisée et ignorée. Chaque prix porte son `xex`.
961
+ */
962
+ async prices(...xexes) {
963
+ if (xexes.length === 0) {
964
+ throw new Error("spot prices() : aucune venue demand\xE9e \u2014 pr\xE9cise au moins un XgateEx.");
965
+ }
966
+ const solo = xexes.length === 1;
967
+ const collected = await Promise.all(
968
+ xexes.map(async (xex) => {
969
+ try {
970
+ return await this.pricesOf(xex);
971
+ } catch (error) {
972
+ if (solo === true) {
973
+ throw error;
974
+ }
975
+ const message = error instanceof Error ? error.message : String(error);
976
+ this.logger.error(`spot prices(${xex}) a \xE9chou\xE9, venue ignor\xE9e : ${message}`);
977
+ return [];
978
+ }
979
+ })
980
+ );
981
+ return collected.flat();
982
+ }
983
+ /** Les prix comptant d'UNE venue, traduits vers {@link IPrice}. */
984
+ async pricesOf(xex) {
985
+ if (servesSpot(xex) === false) {
986
+ throw new Error(
987
+ `spot prices(${xex}) : cette venue ne sert pas de comptant. Disponibles : ${SPOT_XEXES.join(", ")}.`
988
+ );
989
+ }
990
+ const spot = createPublicSpotXex(xex).spot();
991
+ if (spot.getPrices === void 0) {
992
+ throw new Error(`spot prices(${xex}) : cette venue ne sert pas de prix comptant.`);
993
+ }
994
+ const prices = await spot.getPrices();
995
+ return prices.map((price) => ({
996
+ // `canonicalFromXex`, et non `canonicalFromBase` : les prix ne portent QUE le symbole
997
+ // concaténé (`BTCUSDT`), jamais la base — contrairement au catalogue, où la venue déclare
998
+ // `baseAsset`. Passer le symbole à `canonicalFromBase` rendrait `BTCUSDT` tel quel, puisque
999
+ // cette fonction ne strippe volontairement aucune cotation.
1000
+ //
1001
+ // ⚠️ La limite est connue et assumée : le strip ne retire que `USDT`/`USDC` finaux, donc une
1002
+ // paire cotée en BTC (`ROSEBTC`, `ETHBTC`) garde son nom entier. Le catalogue comptant, lui,
1003
+ // résout ces cas — c'est là qu'il faut aller chercher `symbol` et `quote` justes.
1004
+ symbol: canonicalFromXex(price.name, xex),
1005
+ symbolXex: price.name,
1006
+ xex,
1007
+ kind: price.kind,
1008
+ mark: price.mark,
1009
+ oracle: price.oracle,
1010
+ mid: price.mid,
1011
+ bid: price.bid,
1012
+ ask: price.ask,
1013
+ last: price.last,
1014
+ funding: price.funding,
1015
+ openInterest: price.openInterest,
1016
+ volume24h: price.volume24h,
1017
+ prevDayPrice: price.prevDayPrice,
1018
+ quotedAt: price.quotedAt,
1019
+ xtras: price.xtras
1020
+ }));
1021
+ }
1022
+ };
1023
+ exports.SpotPricesService = __decorateClass([
1024
+ common.Injectable()
1025
+ ], exports.SpotPricesService);
1026
+ exports.SpotWsCandlesService = class SpotWsCandlesService {
1027
+ logger = new common.Logger(exports.SpotWsCandlesService.name);
1028
+ /** Les venues dont XGate diffuse le comptant en temps réel. */
1029
+ venues() {
1030
+ return SPOT_XEXES;
1031
+ }
1032
+ /**
1033
+ * Souscrit aux bougies **comptant** d'un marché. Rend la fonction de désabonnement.
1034
+ *
1035
+ * Le handler reçoit une bougie à la fois, au format unifié ; la bougie en cours est repoussée à
1036
+ * chaque mise à jour tant qu'elle n'est pas close.
1037
+ */
1038
+ subscribe(xex, query, handler) {
1039
+ if (servesSpot(xex) === false) {
1040
+ throw new Error(
1041
+ `spot subscribe(${xex}) : cette venue ne sert pas de comptant. Disponibles : ${SPOT_XEXES.join(", ")}.`
1042
+ );
1043
+ }
1044
+ this.logger.log(`souscription bougies COMPTANT ${query.symbol} ${query.interval} sur ${xex}`);
1045
+ return createPublicSpotXex(xex).ws("spot").subscribeCandles({ name: query.symbol, interval: query.interval }, (wire) => {
1046
+ handler(toCandle(wire, xex, query.interval, ""));
1047
+ });
1048
+ }
1049
+ /**
1050
+ * Souscrit au MÊME marché comptant chez plusieurs venues, avec un seul handler.
1051
+ *
1052
+ * Chaque bougie porte son `xex`. La fonction rendue coupe tous les flux d'un coup.
1053
+ */
1054
+ subscribeAll(xexes, query, handler) {
1055
+ if (xexes.length === 0) {
1056
+ throw new Error("spot subscribeAll() : aucune venue demand\xE9e \u2014 pr\xE9cise au moins un XgateEx.");
1057
+ }
1058
+ const stops = [];
1059
+ for (const xex of xexes) {
1060
+ try {
1061
+ stops.push(this.subscribe(xex, query, handler));
1062
+ } catch (error) {
1063
+ const message = error instanceof Error ? error.message : String(error);
1064
+ this.logger.error(`souscription comptant ${xex} refus\xE9e, venue ignor\xE9e : ${message}`);
1065
+ }
1066
+ }
1067
+ return () => {
1068
+ for (const stop of stops) {
1069
+ stop();
1070
+ }
1071
+ };
1072
+ }
1073
+ };
1074
+ exports.SpotWsCandlesService = __decorateClass([
1075
+ common.Injectable()
1076
+ ], exports.SpotWsCandlesService);
801
1077
 
802
1078
  // src/helpers/protection.ts
803
1079
  var SLIPPAGE_DEFAUT = 5e-3;
@@ -1342,29 +1618,37 @@ exports.XgateModule = __decorateClass([
1342
1618
  providers: [
1343
1619
  exports.CatalogService,
1344
1620
  exports.CandlesService,
1621
+ exports.CandlesStreamRegistry,
1345
1622
  exports.WsCandlesService,
1346
1623
  exports.PricesService,
1347
1624
  exports.SpotCatalogService,
1348
1625
  exports.SpotCandlesService,
1626
+ exports.SpotPricesService,
1627
+ exports.SpotWsCandlesService,
1349
1628
  exports.WalletService
1350
1629
  ],
1351
1630
  exports: [
1352
1631
  exports.CatalogService,
1353
1632
  exports.CandlesService,
1633
+ exports.CandlesStreamRegistry,
1354
1634
  exports.WsCandlesService,
1355
1635
  exports.PricesService,
1356
1636
  exports.SpotCatalogService,
1357
1637
  exports.SpotCandlesService,
1638
+ exports.SpotPricesService,
1639
+ exports.SpotWsCandlesService,
1358
1640
  exports.WalletService
1359
1641
  ]
1360
1642
  })
1361
1643
  ], exports.XgateModule);
1362
1644
 
1645
+ exports.CandlesStreamService = CandlesStreamService;
1363
1646
  exports.ORDER_STATUSES = ORDER_STATUSES;
1364
1647
  exports.SPOT_XEXES = SPOT_XEXES;
1365
1648
  exports.TIMEFRAME_MINUTES = TIMEFRAME_MINUTES;
1366
1649
  exports.Timeframe = Timeframe;
1367
1650
  exports.WALLET_XEXES = WALLET_XEXES;
1651
+ exports.WS_SUBSCRIPTION_LIMITS = WS_SUBSCRIPTION_LIMITS;
1368
1652
  exports.XGATE_EXCHANGES = XGATE_EXCHANGES;
1369
1653
  exports.XgateEx = XgateEx;
1370
1654
  exports.buildLevels = buildLevels;
@@ -1379,6 +1663,7 @@ exports.roundToStep = roundToStep;
1379
1663
  exports.servesNatively = servesNatively;
1380
1664
  exports.servesSpot = servesSpot;
1381
1665
  exports.sourceFor = sourceFor;
1666
+ exports.subscriptionLimitOf = subscriptionLimitOf;
1382
1667
  exports.timeframeMs = timeframeMs;
1383
1668
  //# sourceMappingURL=index.cjs.map
1384
1669
  //# sourceMappingURL=index.cjs.map