@blackcube/xgate-sdk 0.21.1 → 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.js CHANGED
@@ -19,135 +19,12 @@ var __decorateClass = (decorators, target, key, kind) => {
19
19
  return result;
20
20
  };
21
21
 
22
- // src/enums/timeframe.enums.ts
23
- var Timeframe = /* @__PURE__ */ ((Timeframe2) => {
24
- Timeframe2["M1"] = "1m";
25
- Timeframe2["M5"] = "5m";
26
- Timeframe2["M15"] = "15m";
27
- Timeframe2["H1"] = "1h";
28
- Timeframe2["H4"] = "4h";
29
- Timeframe2["D"] = "1d";
30
- Timeframe2["W"] = "1w";
31
- return Timeframe2;
32
- })(Timeframe || {});
33
- var TIMEFRAME_MINUTES = {
34
- ["1m" /* M1 */]: 1,
35
- ["5m" /* M5 */]: 5,
36
- ["15m" /* M15 */]: 15,
37
- ["1h" /* H1 */]: 60,
38
- ["4h" /* H4 */]: 240,
39
- ["1d" /* D */]: 1440,
40
- ["1w" /* W */]: 10080
41
- };
42
- function timeframeMs(timeframe) {
43
- return TIMEFRAME_MINUTES[timeframe] * 6e4;
44
- }
45
-
46
- // src/helpers/decimal.ts
47
- function parse(value) {
48
- const trimmed = value.trim();
49
- if (/^-?\d+(\.\d+)?$/.test(trimmed) === false) {
50
- throw new Error(`D\xE9cimal illisible : "${value}".`);
51
- }
52
- const dot = trimmed.indexOf(".");
53
- if (dot < 0) {
54
- return { digits: BigInt(trimmed), scale: 0 };
55
- }
56
- return {
57
- digits: BigInt(trimmed.slice(0, dot) + trimmed.slice(dot + 1)),
58
- scale: trimmed.length - dot - 1
59
- };
60
- }
61
- function format(digits, scale) {
62
- const negative = digits < 0n;
63
- const plain = (negative === true ? -digits : digits).toString().padStart(scale + 1, "0");
64
- const sign = negative === true ? "-" : "";
65
- if (scale === 0) {
66
- return sign + plain;
67
- }
68
- const whole = plain.slice(0, plain.length - scale);
69
- const fraction = plain.slice(plain.length - scale).replace(/0+$/, "");
70
- return fraction === "" ? sign + whole : `${sign + whole}.${fraction}`;
71
- }
72
- function addDecimals(left, right) {
73
- const a = parse(left);
74
- const b = parse(right);
75
- const scale = Math.max(a.scale, b.scale);
76
- const digits = a.digits * 10n ** BigInt(scale - a.scale) + b.digits * 10n ** BigInt(scale - b.scale);
77
- return format(digits, scale);
78
- }
79
- function compareDecimals(left, right) {
80
- const a = parse(left);
81
- const b = parse(right);
82
- const scale = Math.max(a.scale, b.scale);
83
- const x = a.digits * 10n ** BigInt(scale - a.scale);
84
- const y = b.digits * 10n ** BigInt(scale - b.scale);
85
- if (x === y) {
86
- return 0;
87
- }
88
- return x > y ? 1 : -1;
89
- }
90
-
91
- // src/helpers/candle-aggregator.ts
92
- var MONDAY_OFFSET_MS = 4 * 24 * 60 * 60 * 1e3;
93
- function aggregate(source, target, now = Date.now()) {
94
- if (source.length === 0) {
95
- return [];
96
- }
97
- const sourceMs = timeframeMs(source[0]?.interval ?? "1m" /* M1 */);
98
- const targetMs = timeframeMs(target);
99
- if (targetMs <= sourceMs) {
100
- return [];
101
- }
102
- const offsetMs = target === "1w" /* W */ ? MONDAY_OFFSET_MS : 0;
103
- const expected = targetMs / sourceMs;
104
- const buckets = /* @__PURE__ */ new Map();
105
- const sorted = [...source].sort(
106
- (left, right) => left.openedAt.getTime() - right.openedAt.getTime()
107
- );
108
- for (const candle of sorted) {
109
- const start = Math.floor((candle.openedAt.getTime() - offsetMs) / targetMs) * targetMs + offsetMs;
110
- const bucket = buckets.get(start);
111
- if (bucket === void 0) {
112
- buckets.set(start, {
113
- sources: 1,
114
- candle: {
115
- ...candle,
116
- interval: target,
117
- openedAt: new Date(start),
118
- // Dernière milliseconde du créneau, comme les bougies servies par les venues.
119
- closedAt: new Date(start + targetMs - 1),
120
- derived: true
121
- }
122
- });
123
- } else {
124
- bucket.sources += 1;
125
- const built = bucket.candle;
126
- built.high = compareDecimals(candle.high, built.high) > 0 ? candle.high : built.high;
127
- built.low = compareDecimals(candle.low, built.low) < 0 ? candle.low : built.low;
128
- built.close = candle.close;
129
- built.volume = sum(built.volume, candle.volume);
130
- built.quoteVolume = sum(built.quoteVolume, candle.quoteVolume);
131
- built.takerBuyQuoteVolume = sum(built.takerBuyQuoteVolume, candle.takerBuyQuoteVolume);
132
- built.trades = built.trades === null || candle.trades === null ? null : built.trades + candle.trades;
133
- }
134
- }
135
- return [...buckets.keys()].sort((left, right) => left - right).map((start) => buckets.get(start)).filter((bucket) => bucket !== void 0).filter((bucket) => {
136
- const stillOpen = bucket.candle.openedAt.getTime() + targetMs > now;
137
- return bucket.sources >= expected || stillOpen === true;
138
- }).map((bucket) => bucket.candle);
139
- }
140
- function sum(left, right) {
141
- if (left === null || right === null) {
142
- return null;
143
- }
144
- return addDecimals(left, right);
145
- }
146
-
147
22
  // src/helpers/symbol-naming.ts
148
23
  var CONCAT_QUOTE_XEXS = /* @__PURE__ */ new Set(["aster", "binance", "bybit"]);
149
24
  var K_MULTIPLIER = /^k(?=[A-Z])/u;
150
25
  var NUMERIC_MULTIPLIER = /^10{3,}(?=[A-Z0-9])/u;
26
+ var ABBREV_MULTIPLIER = /^1([MK])(?=[A-Z]{2,})/u;
27
+ var ABBREV_FACTOR = { M: 1e6, K: 1e3 };
151
28
  var STOCK_24_5 = /_24_5$/u;
152
29
  var TICKER_ALIASES = {
153
30
  GOOG: "GOOGL",
@@ -172,13 +49,26 @@ function canonicalFromXex(rawSymbol, xexId) {
172
49
  s = s.replace(/-USD-PERP$/u, "").replace(/-PERP$/u, "").replace(/-USDT$/u, "").replace(/-USDC$/u, "").replace(/-USD$/u, "");
173
50
  }
174
51
  s = s.replace(NUMERIC_MULTIPLIER, "");
52
+ s = s.replace(ABBREV_MULTIPLIER, "");
175
53
  s = s.replace(STOCK_24_5, "");
176
54
  return TICKER_ALIASES[s] ?? s;
177
55
  }
178
56
  function canonicalFromBase(base) {
179
- const s = base.replace(K_MULTIPLIER, "").toUpperCase().replace(NUMERIC_MULTIPLIER, "").replace(STOCK_24_5, "");
57
+ const s = base.replace(K_MULTIPLIER, "").toUpperCase().replace(NUMERIC_MULTIPLIER, "").replace(ABBREV_MULTIPLIER, "").replace(STOCK_24_5, "");
180
58
  return TICKER_ALIASES[s] ?? s;
181
59
  }
60
+ function multiplierFromXex(rawSymbol) {
61
+ if (K_MULTIPLIER.test(rawSymbol)) {
62
+ return 1e3;
63
+ }
64
+ const upper = rawSymbol.toUpperCase();
65
+ const abbrev = ABBREV_MULTIPLIER.exec(upper);
66
+ if (abbrev !== null) {
67
+ return ABBREV_FACTOR[abbrev[1]] ?? 1;
68
+ }
69
+ const zeros = NUMERIC_MULTIPLIER.exec(upper);
70
+ return zeros === null ? 1 : Number(zeros[0]);
71
+ }
182
72
 
183
73
  // src/helpers/candle-mapper.ts
184
74
  function toCandle(wire, xex, interval, quote) {
@@ -204,15 +94,13 @@ function toCandle(wire, xex, interval, quote) {
204
94
  };
205
95
  }
206
96
 
207
- // src/helpers/candle-query.ts
208
- function toXexQuery(query) {
209
- return {
210
- name: query.symbol,
211
- interval: query.interval,
212
- startTime: query.startTime,
213
- endTime: query.endTime,
214
- limit: query.limit
215
- };
97
+ // src/helpers/ws-limits.ts
98
+ var WS_SUBSCRIPTION_LIMITS = {
99
+ aster: 200,
100
+ hyperliquid: 1e3
101
+ };
102
+ function subscriptionLimitOf(xex) {
103
+ return WS_SUBSCRIPTION_LIMITS[xex];
216
104
  }
217
105
 
218
106
  // src/enums/xgate-ex.enums.ts
@@ -263,6 +151,19 @@ function createAccountXex(xex, access) {
263
151
  },
264
152
  { default: "a" }
265
153
  );
154
+ case "lighter" /* Lighter */:
155
+ return new Lighter(
156
+ {
157
+ a: {
158
+ apiPrivateKey: access.privateKey ?? NULL_KEY,
159
+ apiKeyIndex: access.apiKeyIndex ?? 0,
160
+ accountIndex: access.accountIndex ?? 0,
161
+ l1Address: adresse(),
162
+ network: access.network ?? "mainnet"
163
+ }
164
+ },
165
+ { default: "a" }
166
+ );
266
167
  case "aster" /* Aster */:
267
168
  if (access.privateKey === void 0 || access.privateKey === "") {
268
169
  throw new Error(
@@ -303,6 +204,7 @@ var WALLET_XEXES = [
303
204
  "hyperliquid" /* Hyperliquid */,
304
205
  "pacifica" /* Pacifica */,
305
206
  "aster" /* Aster */,
207
+ "lighter" /* Lighter */,
306
208
  "blofin" /* Blofin */
307
209
  ];
308
210
  function createPublicXex(xex) {
@@ -346,9 +248,288 @@ function createPublicSpotXex(xex) {
346
248
  }
347
249
  }
348
250
 
251
+ // src/services/candles-stream.service.ts
252
+ var CandlesStreamService = class {
253
+ constructor(xex, kind = "perp") {
254
+ this.xex = xex;
255
+ this.kind = kind;
256
+ this.logger = new Logger(`CandlesStream:${xex}${kind === "spot" ? ":spot" : ""}`);
257
+ this.maxSubscriptions = subscriptionLimitOf(xex) ?? null;
258
+ }
259
+ xex;
260
+ kind;
261
+ logger;
262
+ source = null;
263
+ unsubscribes = [];
264
+ /** Le plafond de souscriptions documenté par la venue, ou `null` si personne ne l'a publié. */
265
+ maxSubscriptions;
266
+ /**
267
+ * Ouvre le flux sur une LISTE de marchés, sur une seule socket.
268
+ *
269
+ * **UNE ERREUR DE FLUX COUPE TOUT, PUIS LÈVE.** Elle n'arrive pas à la souscription : mesuré le
270
+ * 2026-08-07, la venue accepte un symbole inconnu sans broncher, puis ferme la connexion en le
271
+ * découvrant. Le SDK reconnecte, elle referme — cinq cycles en douze secondes, et sur une socket
272
+ * partagée **tous** les marchés valides cessent de recevoir, en silence.
273
+ *
274
+ * On refuse cette boucle : à la première erreur, les souscriptions sont coupées, l'incident est
275
+ * journalisé en `error`, et l'exception part. Un flux à moitié mort qui se tait coûte plus cher
276
+ * qu'un arrêt franc — on ne découvre le premier qu'en constatant l'absence de données.
277
+ *
278
+ * ⚠️ L'exception naît dans un rappel de socket : elle ne remonte pas à l'appelant de `start()`,
279
+ * elle sort en erreur non capturée. C'est délibéré — elle doit être impossible à ignorer.
280
+ *
281
+ * **binance et bybit ne remontent pas encore leurs erreurs** : leur client gère la fermeture en
282
+ * interne sans l'exposer. Sur ces deux venues, la boucle silencieuse reste possible.
283
+ */
284
+ start(symbolsXex, interval, onCandle) {
285
+ const ws = this.wire();
286
+ if ("onError" in ws) {
287
+ ws.onError = (error) => {
288
+ const raison = lisible(error);
289
+ this.logger.error(`${this.xex} : erreur de flux \u2014 ${raison}. Souscriptions coup\xE9es.`);
290
+ this.stop();
291
+ throw new Error(`stream(${this.xex}) : flux interrompu \u2014 ${raison}`);
292
+ };
293
+ }
294
+ for (const symbolXex of symbolsXex) {
295
+ this.unsubscribes.push(
296
+ ws.subscribeCandles({ name: symbolXex, interval }, (wire) => {
297
+ onCandle(toCandle(wire, this.xex, interval, this.quoteOf(symbolXex)));
298
+ })
299
+ );
300
+ }
301
+ this.logger.log(`${this.unsubscribes.length}/${symbolsXex.length} souscription(s) ouvertes`);
302
+ }
303
+ /** Coupe tout. Un désabonnement qui échoue ne doit pas empêcher les autres de se fermer. */
304
+ stop() {
305
+ for (const unsubscribe of this.unsubscribes.splice(0)) {
306
+ try {
307
+ unsubscribe();
308
+ } catch {
309
+ }
310
+ }
311
+ }
312
+ /** Le nombre de souscriptions ouvertes. La liste des désabonnements EST le compteur. */
313
+ subscriptionCount() {
314
+ return this.unsubscribes.length;
315
+ }
316
+ /**
317
+ * Le client temps réel, sur le bon marché.
318
+ *
319
+ * Les deux contrats ne signent pas `ws()` pareil, et c'est voulu : côté comptant le marché est
320
+ * **obligatoire** (`ws('spot')`), parce que les façades ouvrent du perpétuel par défaut — un
321
+ * `ws()` nu y diffuserait des prix de perpétuel à qui croit écouter le comptant.
322
+ */
323
+ wire() {
324
+ const source = this.instance();
325
+ return this.kind === "spot" ? source.ws("spot") : source.ws();
326
+ }
327
+ /** La façade de la venue, créée une seule fois : c'est elle qui porte la socket partagée. */
328
+ instance() {
329
+ if (this.source === null) {
330
+ if (this.kind === "spot" && servesSpot(this.xex) === false) {
331
+ throw new Error(`stream(${this.xex}) : cette venue ne sert pas de comptant.`);
332
+ }
333
+ this.source = this.kind === "spot" ? createPublicSpotXex(this.xex) : createPublicXex(this.xex);
334
+ }
335
+ return this.source;
336
+ }
337
+ /** Voir `CandlesService.quoteOf` : la cotation se déduit du symbole natif, faute d'être publiée. */
338
+ quoteOf(symbolXex) {
339
+ const parts = symbolXex.split("-");
340
+ const last = parts[parts.length - 1];
341
+ return parts.length > 1 && last !== void 0 ? last : "";
342
+ }
343
+ };
344
+ var CandlesStreamRegistry = class {
345
+ streams = /* @__PURE__ */ new Map();
346
+ /** Le flux d'une venue, créé au premier appel puis réutilisé. */
347
+ of(xex, kind = "perp") {
348
+ const cle = `${xex}:${kind}`;
349
+ let stream = this.streams.get(cle);
350
+ if (stream === void 0) {
351
+ stream = new CandlesStreamService(xex, kind);
352
+ this.streams.set(cle, stream);
353
+ }
354
+ return stream;
355
+ }
356
+ /** Coupe tous les flux ouverts, toutes venues confondues. */
357
+ stopAll() {
358
+ for (const stream of this.streams.values()) {
359
+ stream.stop();
360
+ }
361
+ }
362
+ /** Ce qui est ouvert, par venue — pour surveiller sans avoir à tenir de compteur soi-même. */
363
+ counts() {
364
+ const total = {};
365
+ for (const [cle, stream] of this.streams) {
366
+ total[cle] = stream.subscriptionCount();
367
+ }
368
+ return total;
369
+ }
370
+ };
371
+ CandlesStreamRegistry = __decorateClass([
372
+ Injectable()
373
+ ], CandlesStreamRegistry);
374
+ function lisible(error) {
375
+ if (error instanceof Error) {
376
+ return error.message;
377
+ }
378
+ const event = error;
379
+ if (event !== null && typeof event === "object") {
380
+ const message = event.message ?? event.error?.message;
381
+ if (typeof message === "string" && message !== "") {
382
+ return message;
383
+ }
384
+ if (typeof event.type === "string" && event.type !== "") {
385
+ return `\xE9v\xE9nement \xAB ${event.type} \xBB`;
386
+ }
387
+ }
388
+ return String(error);
389
+ }
390
+
391
+ // src/enums/timeframe.enums.ts
392
+ var Timeframe = /* @__PURE__ */ ((Timeframe2) => {
393
+ Timeframe2["M1"] = "1m";
394
+ Timeframe2["M5"] = "5m";
395
+ Timeframe2["M15"] = "15m";
396
+ Timeframe2["H1"] = "1h";
397
+ Timeframe2["H4"] = "4h";
398
+ Timeframe2["D"] = "1d";
399
+ Timeframe2["W"] = "1w";
400
+ return Timeframe2;
401
+ })(Timeframe || {});
402
+ var TIMEFRAME_MINUTES = {
403
+ ["1m" /* M1 */]: 1,
404
+ ["5m" /* M5 */]: 5,
405
+ ["15m" /* M15 */]: 15,
406
+ ["1h" /* H1 */]: 60,
407
+ ["4h" /* H4 */]: 240,
408
+ ["1d" /* D */]: 1440,
409
+ ["1w" /* W */]: 10080
410
+ };
411
+ function timeframeMs(timeframe) {
412
+ return TIMEFRAME_MINUTES[timeframe] * 6e4;
413
+ }
414
+
415
+ // src/helpers/decimal.ts
416
+ function parse(value) {
417
+ const trimmed = value.trim();
418
+ if (/^-?\d+(\.\d+)?$/.test(trimmed) === false) {
419
+ throw new Error(`D\xE9cimal illisible : "${value}".`);
420
+ }
421
+ const dot = trimmed.indexOf(".");
422
+ if (dot < 0) {
423
+ return { digits: BigInt(trimmed), scale: 0 };
424
+ }
425
+ return {
426
+ digits: BigInt(trimmed.slice(0, dot) + trimmed.slice(dot + 1)),
427
+ scale: trimmed.length - dot - 1
428
+ };
429
+ }
430
+ function format(digits, scale) {
431
+ const negative = digits < 0n;
432
+ const plain = (negative === true ? -digits : digits).toString().padStart(scale + 1, "0");
433
+ const sign = negative === true ? "-" : "";
434
+ if (scale === 0) {
435
+ return sign + plain;
436
+ }
437
+ const whole = plain.slice(0, plain.length - scale);
438
+ const fraction = plain.slice(plain.length - scale).replace(/0+$/, "");
439
+ return fraction === "" ? sign + whole : `${sign + whole}.${fraction}`;
440
+ }
441
+ function addDecimals(left, right) {
442
+ const a = parse(left);
443
+ const b = parse(right);
444
+ const scale = Math.max(a.scale, b.scale);
445
+ const digits = a.digits * 10n ** BigInt(scale - a.scale) + b.digits * 10n ** BigInt(scale - b.scale);
446
+ return format(digits, scale);
447
+ }
448
+ function compareDecimals(left, right) {
449
+ const a = parse(left);
450
+ const b = parse(right);
451
+ const scale = Math.max(a.scale, b.scale);
452
+ const x = a.digits * 10n ** BigInt(scale - a.scale);
453
+ const y = b.digits * 10n ** BigInt(scale - b.scale);
454
+ if (x === y) {
455
+ return 0;
456
+ }
457
+ return x > y ? 1 : -1;
458
+ }
459
+
460
+ // src/helpers/candle-aggregator.ts
461
+ var MONDAY_OFFSET_MS = 4 * 24 * 60 * 60 * 1e3;
462
+ function aggregate(source, target, now = Date.now()) {
463
+ if (source.length === 0) {
464
+ return [];
465
+ }
466
+ const sourceMs = timeframeMs(source[0]?.interval ?? "1m" /* M1 */);
467
+ const targetMs = timeframeMs(target);
468
+ if (targetMs <= sourceMs) {
469
+ return [];
470
+ }
471
+ const offsetMs = target === "1w" /* W */ ? MONDAY_OFFSET_MS : 0;
472
+ const expected = targetMs / sourceMs;
473
+ const buckets = /* @__PURE__ */ new Map();
474
+ const sorted = [...source].sort(
475
+ (left, right) => left.openedAt.getTime() - right.openedAt.getTime()
476
+ );
477
+ for (const candle of sorted) {
478
+ const start = Math.floor((candle.openedAt.getTime() - offsetMs) / targetMs) * targetMs + offsetMs;
479
+ const bucket = buckets.get(start);
480
+ if (bucket === void 0) {
481
+ buckets.set(start, {
482
+ sources: 1,
483
+ candle: {
484
+ ...candle,
485
+ interval: target,
486
+ openedAt: new Date(start),
487
+ // Dernière milliseconde du créneau, comme les bougies servies par les venues.
488
+ closedAt: new Date(start + targetMs - 1),
489
+ derived: true
490
+ }
491
+ });
492
+ } else {
493
+ bucket.sources += 1;
494
+ const built = bucket.candle;
495
+ built.high = compareDecimals(candle.high, built.high) > 0 ? candle.high : built.high;
496
+ built.low = compareDecimals(candle.low, built.low) < 0 ? candle.low : built.low;
497
+ built.close = candle.close;
498
+ built.volume = sum(built.volume, candle.volume);
499
+ built.quoteVolume = sum(built.quoteVolume, candle.quoteVolume);
500
+ built.takerBuyQuoteVolume = sum(built.takerBuyQuoteVolume, candle.takerBuyQuoteVolume);
501
+ built.trades = built.trades === null || candle.trades === null ? null : built.trades + candle.trades;
502
+ }
503
+ }
504
+ return [...buckets.keys()].sort((left, right) => left - right).map((start) => buckets.get(start)).filter((bucket) => bucket !== void 0).filter((bucket) => {
505
+ const stillOpen = bucket.candle.openedAt.getTime() + targetMs > now;
506
+ return bucket.sources >= expected || stillOpen === true;
507
+ }).map((bucket) => bucket.candle);
508
+ }
509
+ function sum(left, right) {
510
+ if (left === null || right === null) {
511
+ return null;
512
+ }
513
+ return addDecimals(left, right);
514
+ }
515
+
516
+ // src/helpers/candle-query.ts
517
+ function toXexQuery(query) {
518
+ return {
519
+ name: query.symbol,
520
+ interval: query.interval,
521
+ startTime: query.startTime,
522
+ endTime: query.endTime,
523
+ limit: query.limit
524
+ };
525
+ }
526
+
349
527
  // src/helpers/xex-timeframes.ts
528
+ var HYPERLIQUID_NATIVE = Object.values(Timeframe).filter(
529
+ (interval) => interval !== "1w" /* W */
530
+ );
350
531
  var NATIVE = {
351
- ["hyperliquid" /* Hyperliquid */]: Object.values(Timeframe),
532
+ ["hyperliquid" /* Hyperliquid */]: HYPERLIQUID_NATIVE,
352
533
  ["pacifica" /* Pacifica */]: Object.values(Timeframe),
353
534
  ["aster" /* Aster */]: Object.values(Timeframe),
354
535
  ["blofin" /* Blofin */]: Object.values(Timeframe),
@@ -476,6 +657,43 @@ var CandlesService = class {
476
657
  CandlesService = __decorateClass([
477
658
  Injectable()
478
659
  ], CandlesService);
660
+
661
+ // src/helpers/contract-expiry.ts
662
+ var DATED_CONTRACT_TYPES = /* @__PURE__ */ new Set([
663
+ // binance : les trimestriels (`BTCUSDT_260925`, `ETHUSDT_261225`).
664
+ "CURRENT_QUARTER",
665
+ "NEXT_QUARTER",
666
+ "CURRENT_QUARTER_DELIVERING",
667
+ "NEXT_QUARTER_DELIVERING",
668
+ "PERPETUAL_DELIVERING",
669
+ // bybit : les datés (`BTCUSDT-25DEC26`), face à `LinearPerpetual`.
670
+ "LinearFutures",
671
+ "InverseFutures"
672
+ ]);
673
+ var EXPIRY_FIELDS = ["deliveryTime", "deliveryDate", "expiryTime", "settleTime"];
674
+ function expiryOf(xtras) {
675
+ if (xtras === void 0) {
676
+ return void 0;
677
+ }
678
+ const type = xtras.contractType;
679
+ if (typeof type !== "string" || DATED_CONTRACT_TYPES.has(type) === false) {
680
+ return void 0;
681
+ }
682
+ for (const field of EXPIRY_FIELDS) {
683
+ const brut = xtras[field];
684
+ const ms = typeof brut === "string" ? Number(brut) : brut;
685
+ if (typeof ms === "number" && Number.isFinite(ms) === true && ms > 0) {
686
+ return new Date(ms);
687
+ }
688
+ }
689
+ return void 0;
690
+ }
691
+ function isDated(xtras) {
692
+ const type = xtras?.contractType;
693
+ return typeof type === "string" && DATED_CONTRACT_TYPES.has(type);
694
+ }
695
+
696
+ // src/services/catalog.service.ts
479
697
  var NAMES_ITS_BASE = /* @__PURE__ */ new Set(["binance" /* Binance */, "bybit" /* Bybit */]);
480
698
  var CatalogService = class {
481
699
  logger = new Logger(CatalogService.name);
@@ -521,6 +739,8 @@ var CatalogService = class {
521
739
  symbolXex: pair.name,
522
740
  xex,
523
741
  kind: pair.kind,
742
+ multiplier: multiplierFromXex(pair.name),
743
+ expiresAt: expiryOf(pair.xtras),
524
744
  ref: pair.ref ?? null,
525
745
  szDecimals: pair.szDecimals,
526
746
  tickSize: pair.tickSize,
@@ -595,6 +815,65 @@ var PricesService = class {
595
815
  PricesService = __decorateClass([
596
816
  Injectable()
597
817
  ], PricesService);
818
+ var SpotCandlesService = class {
819
+ logger = new Logger(SpotCandlesService.name);
820
+ /** Les venues dont XGate sert le comptant. */
821
+ venues() {
822
+ return SPOT_XEXES;
823
+ }
824
+ /**
825
+ * Les bougies comptant d'une venue, sur une plage de **dates**.
826
+ *
827
+ * **Lève** si la venue ne sert pas de comptant, plutôt que de rendre une liste vide : « cette
828
+ * venue n'a pas de marché comptant » et « ce marché n'a pas coté » appellent des suites très
829
+ * différentes.
830
+ */
831
+ async candles(xex, query) {
832
+ if (servesSpot(xex) === false) {
833
+ throw new Error(
834
+ `spotCandles(${xex}) : cette venue ne sert pas de comptant. Disponibles : ${SPOT_XEXES.join(", ")}.`
835
+ );
836
+ }
837
+ const spot = createPublicSpotXex(xex).spot();
838
+ if (spot.getCandles === void 0) {
839
+ throw new Error(`spotCandles(${xex}) : cette venue ne sert pas de bougies comptant en REST.`);
840
+ }
841
+ const wires = await spot.getCandles(toXexQuery(query));
842
+ return wires.map((wire) => toCandle(wire, xex, query.interval, ""));
843
+ }
844
+ /**
845
+ * Les bougies comptant d'un même marché chez PLUSIEURS venues, en une liste.
846
+ *
847
+ * Même politique d'échec que partout ailleurs : seule, une venue en échec fait échouer l'appel ;
848
+ * parmi d'autres, elle est journalisée et ignorée. Chaque bougie porte son `xex`.
849
+ */
850
+ async candlesOf(xexes, query) {
851
+ if (xexes.length === 0) {
852
+ throw new Error("spotCandlesOf() : aucune venue demand\xE9e \u2014 pr\xE9cise au moins un XgateEx.");
853
+ }
854
+ const solo = xexes.length === 1;
855
+ const collected = await Promise.all(
856
+ xexes.map(async (xex) => {
857
+ try {
858
+ return await this.candles(xex, query);
859
+ } catch (error) {
860
+ if (solo === true) {
861
+ throw error;
862
+ }
863
+ const message = error instanceof Error ? error.message : String(error);
864
+ this.logger.error(
865
+ `spotCandles(${xex}, ${query.interval}) a \xE9chou\xE9, venue ignor\xE9e : ${message}`
866
+ );
867
+ return [];
868
+ }
869
+ })
870
+ );
871
+ return collected.flat();
872
+ }
873
+ };
874
+ SpotCandlesService = __decorateClass([
875
+ Injectable()
876
+ ], SpotCandlesService);
598
877
  var SpotCatalogService = class {
599
878
  logger = new Logger(SpotCatalogService.name);
600
879
  /** Les venues dont XGate sert le comptant. */
@@ -646,6 +925,9 @@ var SpotCatalogService = class {
646
925
  symbolXex: pair.name,
647
926
  xex,
648
927
  kind: pair.kind,
928
+ // Le comptant porte les mêmes échelles que le perp : `SATSUSDT` (×1) et `1000SATSUSDT`
929
+ // coexistent, et c'est justement entre eux que la comparaison serait fausse.
930
+ multiplier: multiplierFromXex(pair.name),
649
931
  ref: pair.ref ?? null,
650
932
  szDecimals: pair.szDecimals,
651
933
  tickSize: pair.tickSize,
@@ -663,6 +945,133 @@ var SpotCatalogService = class {
663
945
  SpotCatalogService = __decorateClass([
664
946
  Injectable()
665
947
  ], SpotCatalogService);
948
+ var SpotPricesService = class {
949
+ logger = new Logger(SpotPricesService.name);
950
+ /** Les venues dont XGate sert les prix comptant. */
951
+ venues() {
952
+ return SPOT_XEXES;
953
+ }
954
+ /**
955
+ * Les prix comptant d'une ou plusieurs venues, en une seule liste.
956
+ *
957
+ * Même politique d'échec que partout : seule, une venue en échec fait échouer l'appel ; parmi
958
+ * d'autres, elle est journalisée et ignorée. Chaque prix porte son `xex`.
959
+ */
960
+ async prices(...xexes) {
961
+ if (xexes.length === 0) {
962
+ throw new Error("spot prices() : aucune venue demand\xE9e \u2014 pr\xE9cise au moins un XgateEx.");
963
+ }
964
+ const solo = xexes.length === 1;
965
+ const collected = await Promise.all(
966
+ xexes.map(async (xex) => {
967
+ try {
968
+ return await this.pricesOf(xex);
969
+ } catch (error) {
970
+ if (solo === true) {
971
+ throw error;
972
+ }
973
+ const message = error instanceof Error ? error.message : String(error);
974
+ this.logger.error(`spot prices(${xex}) a \xE9chou\xE9, venue ignor\xE9e : ${message}`);
975
+ return [];
976
+ }
977
+ })
978
+ );
979
+ return collected.flat();
980
+ }
981
+ /** Les prix comptant d'UNE venue, traduits vers {@link IPrice}. */
982
+ async pricesOf(xex) {
983
+ if (servesSpot(xex) === false) {
984
+ throw new Error(
985
+ `spot prices(${xex}) : cette venue ne sert pas de comptant. Disponibles : ${SPOT_XEXES.join(", ")}.`
986
+ );
987
+ }
988
+ const spot = createPublicSpotXex(xex).spot();
989
+ if (spot.getPrices === void 0) {
990
+ throw new Error(`spot prices(${xex}) : cette venue ne sert pas de prix comptant.`);
991
+ }
992
+ const prices = await spot.getPrices();
993
+ return prices.map((price) => ({
994
+ // `canonicalFromXex`, et non `canonicalFromBase` : les prix ne portent QUE le symbole
995
+ // concaténé (`BTCUSDT`), jamais la base — contrairement au catalogue, où la venue déclare
996
+ // `baseAsset`. Passer le symbole à `canonicalFromBase` rendrait `BTCUSDT` tel quel, puisque
997
+ // cette fonction ne strippe volontairement aucune cotation.
998
+ //
999
+ // ⚠️ La limite est connue et assumée : le strip ne retire que `USDT`/`USDC` finaux, donc une
1000
+ // paire cotée en BTC (`ROSEBTC`, `ETHBTC`) garde son nom entier. Le catalogue comptant, lui,
1001
+ // résout ces cas — c'est là qu'il faut aller chercher `symbol` et `quote` justes.
1002
+ symbol: canonicalFromXex(price.name, xex),
1003
+ symbolXex: price.name,
1004
+ xex,
1005
+ kind: price.kind,
1006
+ mark: price.mark,
1007
+ oracle: price.oracle,
1008
+ mid: price.mid,
1009
+ bid: price.bid,
1010
+ ask: price.ask,
1011
+ last: price.last,
1012
+ funding: price.funding,
1013
+ openInterest: price.openInterest,
1014
+ volume24h: price.volume24h,
1015
+ prevDayPrice: price.prevDayPrice,
1016
+ quotedAt: price.quotedAt,
1017
+ xtras: price.xtras
1018
+ }));
1019
+ }
1020
+ };
1021
+ SpotPricesService = __decorateClass([
1022
+ Injectable()
1023
+ ], SpotPricesService);
1024
+ var SpotWsCandlesService = class {
1025
+ logger = new Logger(SpotWsCandlesService.name);
1026
+ /** Les venues dont XGate diffuse le comptant en temps réel. */
1027
+ venues() {
1028
+ return SPOT_XEXES;
1029
+ }
1030
+ /**
1031
+ * Souscrit aux bougies **comptant** d'un marché. Rend la fonction de désabonnement.
1032
+ *
1033
+ * Le handler reçoit une bougie à la fois, au format unifié ; la bougie en cours est repoussée à
1034
+ * chaque mise à jour tant qu'elle n'est pas close.
1035
+ */
1036
+ subscribe(xex, query, handler) {
1037
+ if (servesSpot(xex) === false) {
1038
+ throw new Error(
1039
+ `spot subscribe(${xex}) : cette venue ne sert pas de comptant. Disponibles : ${SPOT_XEXES.join(", ")}.`
1040
+ );
1041
+ }
1042
+ this.logger.log(`souscription bougies COMPTANT ${query.symbol} ${query.interval} sur ${xex}`);
1043
+ return createPublicSpotXex(xex).ws("spot").subscribeCandles({ name: query.symbol, interval: query.interval }, (wire) => {
1044
+ handler(toCandle(wire, xex, query.interval, ""));
1045
+ });
1046
+ }
1047
+ /**
1048
+ * Souscrit au MÊME marché comptant chez plusieurs venues, avec un seul handler.
1049
+ *
1050
+ * Chaque bougie porte son `xex`. La fonction rendue coupe tous les flux d'un coup.
1051
+ */
1052
+ subscribeAll(xexes, query, handler) {
1053
+ if (xexes.length === 0) {
1054
+ throw new Error("spot subscribeAll() : aucune venue demand\xE9e \u2014 pr\xE9cise au moins un XgateEx.");
1055
+ }
1056
+ const stops = [];
1057
+ for (const xex of xexes) {
1058
+ try {
1059
+ stops.push(this.subscribe(xex, query, handler));
1060
+ } catch (error) {
1061
+ const message = error instanceof Error ? error.message : String(error);
1062
+ this.logger.error(`souscription comptant ${xex} refus\xE9e, venue ignor\xE9e : ${message}`);
1063
+ }
1064
+ }
1065
+ return () => {
1066
+ for (const stop of stops) {
1067
+ stop();
1068
+ }
1069
+ };
1070
+ }
1071
+ };
1072
+ SpotWsCandlesService = __decorateClass([
1073
+ Injectable()
1074
+ ], SpotWsCandlesService);
666
1075
 
667
1076
  // src/helpers/protection.ts
668
1077
  var SLIPPAGE_DEFAUT = 5e-3;
@@ -1207,22 +1616,30 @@ XgateModule = __decorateClass([
1207
1616
  providers: [
1208
1617
  CatalogService,
1209
1618
  CandlesService,
1619
+ CandlesStreamRegistry,
1210
1620
  WsCandlesService,
1211
1621
  PricesService,
1212
1622
  SpotCatalogService,
1623
+ SpotCandlesService,
1624
+ SpotPricesService,
1625
+ SpotWsCandlesService,
1213
1626
  WalletService
1214
1627
  ],
1215
1628
  exports: [
1216
1629
  CatalogService,
1217
1630
  CandlesService,
1631
+ CandlesStreamRegistry,
1218
1632
  WsCandlesService,
1219
1633
  PricesService,
1220
1634
  SpotCatalogService,
1635
+ SpotCandlesService,
1636
+ SpotPricesService,
1637
+ SpotWsCandlesService,
1221
1638
  WalletService
1222
1639
  ]
1223
1640
  })
1224
1641
  ], XgateModule);
1225
1642
 
1226
- export { CandlesService, CatalogService, ORDER_STATUSES, PricesService, SPOT_XEXES, SpotCatalogService, TIMEFRAME_MINUTES, Timeframe, TradingService, WALLET_XEXES, WalletService, WsCandlesService, XGATE_EXCHANGES, XgateEx, XgateModule, buildLevels, buildProtection, canonicalFromBase, canonicalFromXex, roundPrice, roundToStep, servesNatively, servesSpot, sourceFor, timeframeMs };
1643
+ export { CandlesService, CandlesStreamRegistry, CandlesStreamService, CatalogService, ORDER_STATUSES, PricesService, SPOT_XEXES, SpotCandlesService, SpotCatalogService, SpotPricesService, SpotWsCandlesService, TIMEFRAME_MINUTES, Timeframe, TradingService, WALLET_XEXES, WS_SUBSCRIPTION_LIMITS, WalletService, WsCandlesService, XGATE_EXCHANGES, XgateEx, XgateModule, buildLevels, buildProtection, canonicalFromBase, canonicalFromXex, expiryOf, isDated, multiplierFromXex, roundPrice, roundToStep, servesNatively, servesSpot, sourceFor, subscriptionLimitOf, timeframeMs };
1227
1644
  //# sourceMappingURL=index.js.map
1228
1645
  //# sourceMappingURL=index.js.map