@blackcube/xgate-sdk 0.24.0 → 0.25.1

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/README.md CHANGED
@@ -5,6 +5,46 @@ un catalogue, des bougies, des prix, un portefeuille et du trading — quelle qu
5
5
 
6
6
  > **SDK communautaire / non officiel.** Non affilié aux exchanges couverts. Usage à vos risques.
7
7
 
8
+ ## ⚠️ Si ça ne passe pas par XGate, arrête-toi
9
+
10
+ **XGate est la seule porte vers les venues.** Importer un `@blackcube/*-sdk` directement dans une
11
+ application, c'est contourner — et un contournement ne se voit pas : le code compile, les tests
12
+ passent, et la divergence n'apparaît qu'en production, sur la venue que personne n'a exercée.
13
+
14
+ Tu t'apprêtes à écrire quelque chose qui ne passe pas par XGate ? **Il n'y a que deux cas, et
15
+ aucun des deux ne consiste à contourner.**
16
+
17
+ **1. Tu n'as pas trouvé — c'est le cas le plus fréquent.** La méthode existe sous un autre nom. Le
18
+ vocabulaire d'XGate n'est celui d'aucune venue : il est unifié.
19
+
20
+ | tu cherches | c'est | et pas |
21
+ |---|---|---|
22
+ | passer un ordre | `openWithProtection` | `place` — qui n'existe pas ici |
23
+ | fermer | `closePosition` | un ordre marché `reduceOnly` à la main |
24
+ | modifier un stop | `moveStop` | annuler puis reposer |
25
+ | le levier | `setLeverage` | `updateLeverage` |
26
+ | l'état du compte | `accountInfo` | `getAccountInfo` |
27
+ | l'historique des ordres | `orderHistory` | `getHistory` |
28
+ | le funding payé | `fundingHistory` | `getFundingHistory` |
29
+ | les exécutions en direct | `WsTradesService.subscribe` | `subscribeUserTrades` |
30
+ | N marchés en temps réel | `CandlesStreamRegistry.of()` | N × `WsCandlesService` |
31
+
32
+ **Vérifie avant de conclure** : `grep -rn "public " node_modules/@blackcube/xgate-sdk/dist/index.d.ts`
33
+ ou la table des services plus bas. Ce README documente **35 méthodes publiques sur 35** — si tu ne
34
+ trouves pas, cherche encore.
35
+
36
+ **2. C'est un vrai trou.** Alors **dis-le, et discutes-en** — ne le comble pas dans ton coin.
37
+ Certaines absences sont des **décisions** : `place`, `placeProtection`, `cancelProtection`,
38
+ `subscribeAllCandles`, `subscribePrices` n'existent pas *exprès*, et les raisons sont écrites plus
39
+ bas. Les recréer côté application, c'est réintroduire ce qui a été retiré après incident.
40
+
41
+ D'autres absences sont réelles — `getOrderBook`, `edit`, `getExchangeInfo`, `withdraw`,
42
+ `subscribeOrderBook`, `subscribeBbo`. Elles se comblent **dans XGate**, où elles profitent à tous
43
+ les appelants et sont exercées contre les venues, jamais dans l'application.
44
+
45
+ > **Le réflexe qui trahit le contournement** : tu es sur le point d'écrire `import { Hyperliquid }
46
+ > from '@blackcube/hyperliquid-sdk'` dans du code applicatif. Arrête-toi là.
47
+
8
48
  ## Installation
9
49
 
10
50
  ```bash
@@ -230,12 +270,28 @@ venues.
230
270
  ## Le trading
231
271
 
232
272
  **Jamais un ordre sans stop.** `openWithProtection` est la seule façon d'ouvrir une position, et
233
- elle exige un `slPct`. La protection part **avec** l'entrée, en un geste que la venue traite
273
+ elle exige un `sl`. La protection part **avec** l'entrée, en un geste que la venue traite
234
274
  atomiquement : si l'entrée ne remplit pas, la protection ne naît jamais.
235
275
 
236
- **L'appelant donne des pourcentages, jamais des prix.** XGate calcule les niveaux depuis le prix
237
- d'entrée, les arrondit à la grille de la paire, et garantit qu'au-delà de 80 % de parts cumulées la
238
- position ferme entièrement — sans reliquat de 0,01 qui traîne.
276
+ ### Un niveau se dit en écart OU en prix
277
+
278
+ ```typescript
279
+ sl: { pct: 0.02 } // 2 % sous l'entrée en long, 2 % au-dessus en short
280
+ sl: { price: 62500 } // exactement 62 500, quel que soit le prix d'entrée
281
+ ```
282
+
283
+ Les deux façons de penser un objectif coexistent réellement : « je sors si ça monte de 10 % »
284
+ raisonne en **risque**, « je sors si ça touche 62 500 » raisonne en **niveau technique**. Obliger à
285
+ convertir l'un dans l'autre reviendrait à faire calculer l'appelant, donc à déplacer l'erreur chez
286
+ lui. Les deux modes se mélangent librement dans le même appel.
287
+
288
+ **`part` reste toujours une fraction de la POSITION** — les deux modes ne portent que sur le niveau.
289
+
290
+ Le type rend l'ambiguïté impossible : `ITarget` est une union **exclusive**, donc
291
+ `{ pct: 0.1, price: 62500 }` **ne compile pas**, et `{}` non plus.
292
+
293
+ **XGate arrondit les niveaux à la grille de la paire** et garantit qu'au-delà de 80 % de parts
294
+ cumulées la position ferme entièrement — sans reliquat de 0,01 qui traîne.
239
295
 
240
296
  ```typescript
241
297
  import { TradingService, XgateEx } from '@blackcube/xgate-sdk';
@@ -254,9 +310,9 @@ const orders = await trading.openWithProtection(access, {
254
310
  direction: 'long',
255
311
  size: 0.02,
256
312
  entry: 3120,
257
- slPct: 0.02, // stop à 2 % sous l'entrée
313
+ sl: { pct: 0.02 }, // stop à 2 % sous l'entrée
258
314
  tps: [{ pct: 0.03, part: 0.5 }, // 50 % à +3 %
259
- { pct: 0.06, part: 0.5 }], // 50 % à +6 % → somme 100 %, clôture intégrale
315
+ { price: 62500, part: 0.5 }], // 50 % si le prix touche 62 500
260
316
  tif: 'ioc',
261
317
  });
262
318
  ```
package/dist/index.cjs CHANGED
@@ -97,6 +97,24 @@ function toCandle(wire, xex, interval, quote) {
97
97
  };
98
98
  }
99
99
 
100
+ // src/helpers/socket-error.ts
101
+ function lisible(error) {
102
+ if (error instanceof Error) {
103
+ return error.message;
104
+ }
105
+ const event = error;
106
+ if (event !== null && typeof event === "object") {
107
+ const message = event.message ?? event.error?.message;
108
+ if (typeof message === "string" && message !== "") {
109
+ return message;
110
+ }
111
+ if (typeof event.type === "string" && event.type !== "") {
112
+ return `\xE9v\xE9nement \xAB ${event.type} \xBB`;
113
+ }
114
+ }
115
+ return String(error);
116
+ }
117
+
100
118
  // src/helpers/ws-limits.ts
101
119
  var WS_SUBSCRIPTION_LIMITS = {
102
120
  aster: 200,
@@ -266,20 +284,36 @@ var CandlesStreamService = class {
266
284
  unsubscribes = [];
267
285
  /** Le plafond de souscriptions documenté par la venue, ou `null` si personne ne l'a publié. */
268
286
  maxSubscriptions;
287
+ /**
288
+ * Notifié quand le flux meurt — après que les souscriptions ont été coupées.
289
+ *
290
+ * **Ce crochet a remplacé un `throw`.** Lever depuis un rappel de socket ne remonte à aucun
291
+ * appelant : l'exception sort en `unhandledRejection` et **tue le process entier**, ce qui est
292
+ * arrivé le 2026-08-13 sur Blips par un autre chemin. « Impossible à ignorer » et « fatal pour
293
+ * tout le monde » ne sont pas la même chose : la première est une garantie, la seconde une panne.
294
+ *
295
+ * Le contrat reste franc — le flux est coupé, il ne repartira pas tout seul, et le silence est
296
+ * journalisé en `.error` de toute façon. Mais c'est à l'appelant de décider ce qu'il en fait :
297
+ * rouvrir, basculer de venue, ou s'arrêter.
298
+ */
299
+ onFailure = null;
269
300
  /**
270
301
  * Ouvre le flux sur une LISTE de marchés, sur une seule socket.
271
302
  *
272
- * **UNE ERREUR DE FLUX COUPE TOUT, PUIS LÈVE.** Elle n'arrive pas à la souscription : mesuré le
303
+ * **UNE ERREUR DE FLUX COUPE TOUT, PUIS NOTIFIE.** Elle n'arrive pas à la souscription : mesuré le
273
304
  * 2026-08-07, la venue accepte un symbole inconnu sans broncher, puis ferme la connexion en le
274
305
  * découvrant. Le SDK reconnecte, elle referme — cinq cycles en douze secondes, et sur une socket
275
306
  * partagée **tous** les marchés valides cessent de recevoir, en silence.
276
307
  *
277
308
  * On refuse cette boucle : à la première erreur, les souscriptions sont coupées, l'incident est
278
- * journalisé en `error`, et l'exception part. Un flux à moitié mort qui se tait coûte plus cher
279
- * qu'un arrêt franc — on ne découvre le premier qu'en constatant l'absence de données.
309
+ * journalisé en `error`, et {@link onFailure} est notifié. Un flux à moitié mort qui se tait
310
+ * coûte plus cher qu'un arrêt franc — on ne découvre le premier qu'en constatant l'absence de
311
+ * données.
280
312
  *
281
- * ⚠️ L'exception naît dans un rappel de socket : elle ne remonte pas à l'appelant de `start()`,
282
- * elle sort en erreur non capturée. C'est délibéré — elle doit être impossible à ignorer.
313
+ * ⚠️ **On ne lève pas depuis le rappel de socket.** Une exception née là ne remonte à aucun
314
+ * appelant de `start()` : elle sort en `unhandledRejection` et emporte le process entier — pas
315
+ * seulement le flux fautif, mais les crons et tout ce qui tournait à côté. C'est {@link onFailure}
316
+ * qui porte l'information, et l'appelant qui décide.
283
317
  *
284
318
  * **binance et bybit ne remontent pas encore leurs erreurs** : leur client gère la fermeture en
285
319
  * interne sans l'exposer. Sur ces deux venues, la boucle silencieuse reste possible.
@@ -291,7 +325,7 @@ var CandlesStreamService = class {
291
325
  const raison = lisible(error);
292
326
  this.logger.error(`${this.xex} : erreur de flux \u2014 ${raison}. Souscriptions coup\xE9es.`);
293
327
  this.stop();
294
- throw new Error(`stream(${this.xex}) : flux interrompu \u2014 ${raison}`);
328
+ this.onFailure?.(new Error(`stream(${this.xex}) : flux interrompu \u2014 ${raison}`));
295
329
  };
296
330
  }
297
331
  for (const symbolXex of symbolsXex) {
@@ -374,22 +408,6 @@ exports.CandlesStreamRegistry = class CandlesStreamRegistry {
374
408
  exports.CandlesStreamRegistry = __decorateClass([
375
409
  common.Injectable()
376
410
  ], exports.CandlesStreamRegistry);
377
- function lisible(error) {
378
- if (error instanceof Error) {
379
- return error.message;
380
- }
381
- const event = error;
382
- if (event !== null && typeof event === "object") {
383
- const message = event.message ?? event.error?.message;
384
- if (typeof message === "string" && message !== "") {
385
- return message;
386
- }
387
- if (typeof event.type === "string" && event.type !== "") {
388
- return `\xE9v\xE9nement \xAB ${event.type} \xBB`;
389
- }
390
- }
391
- return String(error);
392
- }
393
411
 
394
412
  // src/enums/timeframe.enums.ts
395
413
  var Timeframe = /* @__PURE__ */ ((Timeframe2) => {
@@ -1077,6 +1095,13 @@ exports.SpotWsCandlesService = __decorateClass([
1077
1095
  ], exports.SpotWsCandlesService);
1078
1096
 
1079
1097
  // src/helpers/protection.ts
1098
+ function priceOf(cible, entry, direction, gagnant) {
1099
+ if (cible.price !== void 0) {
1100
+ return round8(cible.price);
1101
+ }
1102
+ const dir = (direction === "long" ? 1 : -1) * (gagnant === true ? 1 : -1);
1103
+ return round8(entry * (1 + dir * cible.pct));
1104
+ }
1080
1105
  var SLIPPAGE_DEFAUT = 5e-3;
1081
1106
  var SEUIL_CLOTURE = 0.8;
1082
1107
  function round8(value) {
@@ -1119,12 +1144,8 @@ function buildLevels(entry, direction, slPct, tpPcts) {
1119
1144
  function buildProtection(input) {
1120
1145
  const slippage = input.slippagePct ?? SLIPPAGE_DEFAUT;
1121
1146
  const long = input.direction === "long";
1122
- const { sl, tps: niveaux } = buildLevels(
1123
- input.entry,
1124
- input.direction,
1125
- input.slPct,
1126
- input.tps.map((tp) => tp.pct)
1127
- );
1147
+ const sl = priceOf(input.sl, input.entry, input.direction, false);
1148
+ const niveaux = input.tps.map((tp) => priceOf(tp, input.entry, input.direction, true));
1128
1149
  const borne = (trigger) => long === true ? trigger * (1 - slippage) : trigger * (1 + slippage);
1129
1150
  const sommeParts = input.tps.reduce((total, tp) => total + tp.part, 0);
1130
1151
  const clotureIntegrale = sommeParts > SEUIL_CLOTURE;
@@ -1195,7 +1216,7 @@ exports.TradingService = class TradingService {
1195
1216
  entry: input.entry,
1196
1217
  direction: input.direction,
1197
1218
  size: input.size,
1198
- slPct: input.slPct,
1219
+ sl: input.sl,
1199
1220
  tps: input.tps,
1200
1221
  tickSize: input.tickSize,
1201
1222
  lotSize: input.lotSize,
@@ -1818,6 +1839,8 @@ exports.WalletService = __decorateClass([
1818
1839
  ], exports.WalletService);
1819
1840
  exports.WsCandlesService = class WsCandlesService {
1820
1841
  logger = new common.Logger(exports.WsCandlesService.name);
1842
+ /** Un client temps réel par venue — voir {@link wireOf}. La clé EST la venue. */
1843
+ wires = /* @__PURE__ */ new Map();
1821
1844
  /**
1822
1845
  * Souscrit aux bougies d'un marché chez une venue. Rend la fonction de désabonnement.
1823
1846
  *
@@ -1833,9 +1856,12 @@ exports.WsCandlesService = class WsCandlesService {
1833
1856
  subscribe(xex, query, handler) {
1834
1857
  const quote = this.quoteOf(query.symbol);
1835
1858
  this.logger.log(`souscription bougies ${query.symbol} ${query.interval} sur ${xex}`);
1836
- return createPublicXex(xex).ws().subscribeCandles({ name: query.symbol, interval: query.interval }, (wire) => {
1837
- handler(toCandle(wire, xex, query.interval, quote));
1838
- });
1859
+ return this.wireOf(xex).subscribeCandles(
1860
+ { name: query.symbol, interval: query.interval },
1861
+ (wire) => {
1862
+ handler(toCandle(wire, xex, query.interval, quote));
1863
+ }
1864
+ );
1839
1865
  }
1840
1866
  /**
1841
1867
  * Souscrit au MÊME marché chez plusieurs venues, avec un seul handler.
@@ -1862,6 +1888,35 @@ exports.WsCandlesService = class WsCandlesService {
1862
1888
  }
1863
1889
  };
1864
1890
  }
1891
+ /**
1892
+ * Le client temps réel d'une venue — **créé une seule fois, donc UNE SOCKET PAR VENUE**.
1893
+ *
1894
+ * C'est la règle que `CandlesStreamService` énonce déjà et que ce service violait : chaque
1895
+ * `subscribe()` appelait `createPublicXex(xex)`, qui construit une façade NEUVE. Or la façade
1896
+ * mémoïse son client temps réel (`unifiedWs()`), et c'est lui qui porte la socket : une façade
1897
+ * par souscription, c'est une socket par souscription.
1898
+ *
1899
+ * Mesuré le 2026-08-13 sur Blips : 1 104 souscriptions ont ouvert **1 104 sockets** sur sept
1900
+ * venues, quand hyperliquid n'en accepte que 10 par IP. Les descripteurs s'épuisent, les venues
1901
+ * refusent, et chaque connexion refusée rejetait une promesse que personne n'attendait — le
1902
+ * process entier mourait deux minutes après l'ouverture des flux. Ici : **sept sockets**.
1903
+ *
1904
+ * Rien à fermer explicitement : le ref-count vit dans les SDK (`refs += 1` / `release()`), donc
1905
+ * la socket se ferme d'elle-même au dernier désabonnement de la venue, et se rouvre au suivant.
1906
+ */
1907
+ wireOf(xex) {
1908
+ let wire = this.wires.get(xex);
1909
+ if (wire === void 0) {
1910
+ wire = createPublicXex(xex).ws();
1911
+ if ("onError" in wire) {
1912
+ wire.onError = (error) => {
1913
+ this.logger.error(`${xex} : erreur de flux \u2014 ${lisible(error)}`);
1914
+ };
1915
+ }
1916
+ this.wires.set(xex, wire);
1917
+ }
1918
+ return wire;
1919
+ }
1865
1920
  /** Voir `CandlesService.quoteOf` : la cotation se déduit du symbole natif, faute d'être publiée. */
1866
1921
  quoteOf(symbolXex) {
1867
1922
  const parts = symbolXex.split("-");