@blackcube/xgate-sdk 0.23.0 → 0.25.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/README.md CHANGED
@@ -22,7 +22,7 @@ une capacité n'est comptée que si un test réel l'a exercée.
22
22
  |---|:---:|:---:|:---:|:---:|
23
23
  | **hyperliquid** | ✔ | ✔ | ✔ | ✔ **prouvée** |
24
24
  | **pacifica** | ✔ | ✔ | ✔ | ✔ **prouvée** |
25
- | **aster** | ✔ | ✔ | ✔ | ⚠ **à valider** |
25
+ | **aster** | ✔ | ✔ | ✔ | ✔ **prouvée** *(voir la fenêtre d'1 s)* |
26
26
  | lighter | ✔ | ✔ *(sans clé)* | — | — |
27
27
  | extended | ✔ | — | — | — |
28
28
  | paradex | ✔ | — | — | — |
@@ -34,12 +34,25 @@ une capacité n'est comptée que si un test réel l'a exercée.
34
34
  **hyperliquid et pacifica sont complètes et prouvées.** Le cycle entier — ouvrir avec stop, lire la
35
35
  position, refermer sans reliquat — tourne sur leur testnet à chaque exécution de la suite de tests.
36
36
 
37
- **aster est complète mais non validée**, et le refus est délibéré. Son `/fapi/v3/batchOrders` coupe
38
- à ~3,26 s en répondant « The request has timed out. » *alors que la venue a déjà créé une partie des
39
- ordres* : deux exécutions identiques ont produit deux sous-ensembles différents. Un statut inconnu
40
- sur une ouverture protégée, c'est la position nue que ce paquet existe pour rendre impossible — donc
41
- `openWithProtection` y **lève** tant que la preuve manque. Tout le reste (positions, ordres,
42
- historique, clôture) fonctionne. Détail complet dans le backlog.
37
+ **aster ouvre en SÉQUENTIEL, avec une fenêtre d'environ une seconde.** Son lot atomique
38
+ (`/fapi/v3/batchOrders`) coupe systématiquement à ~3,26 s — chaque leg s'y écrit on-chain — et
39
+ laissait des orphelins non déterministes : une fois l'entrée seule, une fois le stop seul. XGate y
40
+ pose donc l'entrée, **puis** le stop, **puis** les take-profits.
41
+
42
+ ⚠️ **Entre l'entrée remplie et le stop posé, la position est nue pendant ~1,1 s** (mesuré :
43
+ entrée 1 041 ms, stop 1 078 ms plus tard). Si le marché s'effondre dans cet intervalle, rien ne
44
+ protège. C'est un compromis assumé — l'alternative était une venue inutilisable.
45
+
46
+ Poser le stop **en premier** supprimerait la fenêtre, mais aster **accepte un `reduceOnly` sans
47
+ position puis l'annule** : vérifié, il a disparu du carnet une seconde plus tard.
48
+
49
+ **Filet de sécurité** : si le stop échoue alors que l'entrée a rempli, la position est **soldée au
50
+ marché** et l'erreur remonte. Une position sans stop ne survit pas à `openWithProtection`.
51
+
52
+ > **Un mot sur les timeouts d'aster.** Sa documentation annonce un `HTTP 503` pour un timeout, « à
53
+ > ne **PAS** traiter comme un échec ; le statut d'exécution est **INCONNU** ». Elle renvoie en
54
+ > réalité un **`400`** — « requête malformée » — alors que les ordres sont bel et bien créés. XGate
55
+ > reconnaît donc le message, relit le carnet, et lève sur position nue ou balaie les orphelins.
43
56
 
44
57
  **lighter se lit sans aucune clé.** Sa documentation classe `/account`, `accountsByL1Address` et
45
58
  `positions` parmi les routes **sans authentification**, et `/account` accepte « an account's index,
@@ -67,12 +80,21 @@ Chacun est un `@Injectable()` NestJS, utilisable seul ou via `XgateModule`.
67
80
  | catalogue | `CatalogService` | `SpotCatalogService` |
68
81
  | bougies REST | `CandlesService` | `SpotCandlesService` |
69
82
  | prix | `PricesService` | `SpotPricesService` |
70
- | bougies temps réel | `WsCandlesService` | `SpotWsCandlesService` |
71
- | **flux multi-marchés** | `CandlesStreamRegistry` | `CandlesStreamRegistry.of(xex, 'spot')` |
83
+ | temps réel — **plusieurs marchés** | `CandlesStreamRegistry` | `CandlesStreamRegistry.of(xex, 'spot')` |
84
+ | temps réel — **un seul marché** | `WsCandlesService` | `SpotWsCandlesService` |
85
+
86
+ `candlesOf([...venues], query)` interroge le **même marché chez plusieurs venues** en une liste —
87
+ chaque bougie porte son `xex`. Une venue en échec parmi d'autres est journalisée et ignorée ; seule,
88
+ son échec est celui de l'appel.
72
89
 
73
90
  `CandlesService` agrège quand la venue ne sert pas l'intervalle demandé ; les autres servent ce que
74
91
  la venue publie. Un `Spot*` appelé sur une venue sans comptant **lève**.
75
92
 
93
+ ⚠️ **Les deux lignes de temps réel ne sont pas interchangeables.** `WsCandlesService` ouvre **une
94
+ socket par souscription** : suivre 50 marchés y ouvre 50 connexions, quand hyperliquid n'en accepte
95
+ que 10 par IP. Le registre, lui, les fait toutes passer par **une seule**. En cas de doute, prends
96
+ le registre — `WsCandlesService` ne se justifie que pour un ou deux marchés.
97
+
76
98
  ### ⚠️ Les sockets réagissent MAL à un symbole invalide
77
99
 
78
100
  **À lire avant d'ouvrir un flux.** Aucune des venues ne rejette proprement un marché inconnu : elles
@@ -113,12 +135,22 @@ flux.maxSubscriptions; // 1000 — plafond documenté, null si la venue n'e
113
135
  registry.stopAll();
114
136
  ```
115
137
 
116
- `of(xex)` rend **toujours la même instance** pour une venue : deux appels, deux `start()`, une seule
117
- connexion. C'est ce qui rend tenable de suivre 200 marchés — hyperliquid n'accepte que 10 connexions
118
- par IP.
138
+ `of(xex, kind?)` rend **toujours la même instance** pour une venue et un marché : deux appels, deux
139
+ `start()`, une seule connexion. C'est ce qui rend tenable de suivre 200 marchés — hyperliquid
140
+ n'accepte que 10 connexions par IP.
119
141
 
120
- `WsCandlesService`, lui, ouvre **une socket par souscription** : à réserver au suivi d'un ou deux
121
- marchés.
142
+ **Deux classes, deux rôles.** `CandlesStreamRegistry` tient la table des flux ; `CandlesStreamService`
143
+ est le flux lui-même, celui que `of()` rend. Les deux sont exportés, mais on n'instancie normalement
144
+ que le registre — construire un `CandlesStreamService` à la main revient à tenir soi-même la table,
145
+ donc à risquer plusieurs sockets pour la même venue.
146
+
147
+ | | `CandlesStreamRegistry` | `CandlesStreamService` |
148
+ |---|---|---|
149
+ | `of(xex, kind?)` | le flux d'une venue, créé une fois | — |
150
+ | `start(symbols, interval, onCandle)` | — | ouvre les souscriptions |
151
+ | `stop()` / `stopAll()` | coupe tous les flux | coupe ce flux |
152
+ | `subscriptionCount()` / `counts()` | par venue | pour ce flux |
153
+ | `maxSubscriptions` | — | plafond documenté, ou `null` |
122
154
 
123
155
  > **Une erreur de flux coupe tout, puis lève.** Un symbole invalide n'échoue pas à la
124
156
  > souscription : la venue l'accepte, puis ferme la connexion en le découvrant — cinq cycles de
@@ -149,12 +181,14 @@ marchés.
149
181
  |---|---|
150
182
  | `WalletService` | soldes, mouvements, historique d'équité |
151
183
  | `TradingService` | positions, ordres, exécutions — et le cycle **ouvrir → gérer → fermer** |
184
+ | `WsTradesService` | les flux du compte : **exécutions**, **ordres**, et transactions **publiques** |
152
185
 
153
186
  Le cycle de vie d'une position tient en trois gestes, et chacun préserve le même invariant : la
154
187
  position n'est jamais sans stop.
155
188
 
156
189
  | geste | méthode | venues |
157
190
  |---|---|---|
191
+ | régler le levier | `setLeverage` | toutes sauf bullet |
158
192
  | ouvrir avec sa protection | `openWithProtection` | hyperliquid, pacifica |
159
193
  | déplacer le stop | `moveStop` | hyperliquid, pacifica, aster |
160
194
  | fermer sans reliquat | `closePosition` | toutes celles qui tradent |
@@ -162,6 +196,16 @@ position n'est jamais sans stop.
162
196
 
163
197
  ## Le portefeuille
164
198
 
199
+ ```typescript
200
+ await wallet.balances(access); // comptant + collatéral, chaque ligne marquée
201
+ await wallet.movements(access); // dépôts, retraits, transferts — montants signés
202
+ await wallet.equityHistory(access); // la courbe d'équité du compte
203
+ ```
204
+
205
+ `movements` **lève** si la venue ne les sert pas, plutôt que de rendre une liste vide : « cette
206
+ venue n'expose pas son historique » et « ce compte n'a jamais bougé » appellent des suites très
207
+ différentes. `equityHistory` n'est publié que par hyperliquid et pacifica.
208
+
165
209
  `balances()` rend **le comptant et le collatéral**, chaque ligne portant sa nature :
166
210
 
167
211
  ```typescript
@@ -186,12 +230,28 @@ venues.
186
230
  ## Le trading
187
231
 
188
232
  **Jamais un ordre sans stop.** `openWithProtection` est la seule façon d'ouvrir une position, et
189
- elle exige un `slPct`. La protection part **avec** l'entrée, en un geste que la venue traite
233
+ elle exige un `sl`. La protection part **avec** l'entrée, en un geste que la venue traite
190
234
  atomiquement : si l'entrée ne remplit pas, la protection ne naît jamais.
191
235
 
192
- **L'appelant donne des pourcentages, jamais des prix.** XGate calcule les niveaux depuis le prix
193
- d'entrée, les arrondit à la grille de la paire, et garantit qu'au-delà de 80 % de parts cumulées la
194
- position ferme entièrement — sans reliquat de 0,01 qui traîne.
236
+ ### Un niveau se dit en écart OU en prix
237
+
238
+ ```typescript
239
+ sl: { pct: 0.02 } // 2 % sous l'entrée en long, 2 % au-dessus en short
240
+ sl: { price: 62500 } // exactement 62 500, quel que soit le prix d'entrée
241
+ ```
242
+
243
+ Les deux façons de penser un objectif coexistent réellement : « je sors si ça monte de 10 % »
244
+ raisonne en **risque**, « je sors si ça touche 62 500 » raisonne en **niveau technique**. Obliger à
245
+ convertir l'un dans l'autre reviendrait à faire calculer l'appelant, donc à déplacer l'erreur chez
246
+ lui. Les deux modes se mélangent librement dans le même appel.
247
+
248
+ **`part` reste toujours une fraction de la POSITION** — les deux modes ne portent que sur le niveau.
249
+
250
+ Le type rend l'ambiguïté impossible : `ITarget` est une union **exclusive**, donc
251
+ `{ pct: 0.1, price: 62500 }` **ne compile pas**, et `{}` non plus.
252
+
253
+ **XGate arrondit les niveaux à la grille de la paire** et garantit qu'au-delà de 80 % de parts
254
+ cumulées la position ferme entièrement — sans reliquat de 0,01 qui traîne.
195
255
 
196
256
  ```typescript
197
257
  import { TradingService, XgateEx } from '@blackcube/xgate-sdk';
@@ -210,9 +270,9 @@ const orders = await trading.openWithProtection(access, {
210
270
  direction: 'long',
211
271
  size: 0.02,
212
272
  entry: 3120,
213
- slPct: 0.02, // stop à 2 % sous l'entrée
273
+ sl: { pct: 0.02 }, // stop à 2 % sous l'entrée
214
274
  tps: [{ pct: 0.03, part: 0.5 }, // 50 % à +3 %
215
- { pct: 0.06, part: 0.5 }], // 50 % à +6 % → somme 100 %, clôture intégrale
275
+ { price: 62500, part: 0.5 }], // 50 % si le prix touche 62 500
216
276
  tif: 'ioc',
217
277
  });
218
278
  ```
@@ -251,6 +311,89 @@ l'erreur ne se voit qu'une fois l'argent parti. Le prix courant est donc un argu
251
311
  > une chaîne nue, sans identifiant — pour un stop groupé sous son entrée. Il se retrouve au carnet,
252
312
  > où `reduceOnly === true` le distingue.
253
313
 
314
+ ### Savoir ce qui s'est rempli : `WsTradesService`
315
+
316
+ Le flux des exécutions du compte, en temps réel. C'est ce qui permet de connaître son état **exact**
317
+ sans sonder : entre deux interrogations, un take-profit partiel a pu changer la taille de la
318
+ position, et toute décision prise sur l'ancienne valeur est fausse.
319
+
320
+ ```typescript
321
+ const stop = wsTrades.subscribe(access, (trade) => {
322
+ // trade.price est le prix RÉELLEMENT obtenu, pas celui demandé
323
+ });
324
+ ```
325
+
326
+ **Seules les exécutions survenues après l'abonnement sont transmises.** Les venues rejouent leur
327
+ historique à la souscription — mesuré sur hyperliquid : 30 exécutions dans les six premières
328
+ secondes, toutes antérieures, la plus ancienne de plus de deux jours. XGate les écarte, sans quoi
329
+ chaque reconnexion rejouerait deux jours de trades et ferait compter deux fois les mêmes
330
+ remplissages. Pour l'historique, `TradingService.trades()` est fait pour ça.
331
+
332
+ `subscribeAll([...accès], handler)` suit plusieurs comptes avec un seul handler. **blofin ne diffuse
333
+ pas ses exécutions** et lève ; les sept autres venues les servent.
334
+
335
+ **Trois flux, trois usages** :
336
+
337
+ ```typescript
338
+ wsTrades.subscribe(access, (trade) => { … }); // MES exécutions : à quel prix
339
+ wsTrades.subscribeOrders(access, (order) => { … }); // MES ordres : leur cycle de vie
340
+ wsTrades.subscribePublicTrades(access, 'ETH', (trade) => { … }); // le marché : ce qui se négocie
341
+ ```
342
+
343
+ `subscribeOrders` voit l'ordre **naître et changer d'état** (`open` → `filled`, ou `canceled`) —
344
+ c'est là qu'on apprend qu'un stop vient de se déclencher. `subscribe` dit à quel prix. Les deux
345
+ écartent le rejeu d'historique ; `subscribePublicTrades` n'en a pas à écarter.
346
+
347
+ ### La marge : mode, renfort, allègement
348
+
349
+ ```typescript
350
+ await trading.setMarginMode(access, 'ETH', true); // true = isolée, false = croisée
351
+ await trading.addMargin(access, 'ETH', '2'); // éloigne la liquidation
352
+ await trading.removeMargin(access, 'ETH', '1'); // libère du capital
353
+ ```
354
+
355
+ **Le mode se règle AVANT d'ouvrir.** En **croisée**, tout le solde garantit la position : elle tient
356
+ plus longtemps, mais une liquidation emporte l'ensemble. En **isolée**, seule la marge affectée est
357
+ en jeu : la perte est bornée, la liquidation arrive plus tôt.
358
+
359
+ `addMargin` renforce une position sous pression **sans la réduire** — vérifié en réel : marge 38,45
360
+ → 40,46 après `+2`, puis 39,46 après `−1`. N'a de sens qu'en isolée ; en croisée le solde entier
361
+ garantit déjà la position.
362
+
363
+ ⚠️ **pacifica ne retire pas de marge** et `removeMargin` y lève : fermer partiellement la position
364
+ est alors la façon de libérer du capital.
365
+
366
+ ### Le coupe-circuit : `cancelAll`
367
+
368
+ Annule **tous** les ordres d'une paire d'un geste — pour reprendre la main sans discuter : état
369
+ incohérent, arrêt d'urgence, reprise après incident. Annuler un par un laisse une fenêtre pendant
370
+ laquelle certains vivent encore.
371
+
372
+ ```typescript
373
+ const annules = await trading.cancelAll(access, 'ETH'); // nombre annulé, ou null
374
+ ```
375
+
376
+ ⚠️ **Cela retire aussi les protections.** Stops et take-profits sont des ordres comme les autres :
377
+ une position ouverte se retrouve **nue** après ce geste. XGate le journalise en `warn` s'il détecte
378
+ une position, mais ne refuse pas — c'est un coupe-circuit, il doit couper. À n'employer que si la
379
+ position est fermée, ou en reposant une protection dans la foulée.
380
+
381
+ ### Ne jamais annuler pour reposer
382
+
383
+ **Annuler une protection pour la recréer est une aberration**, et XGate n'expose délibérément ni
384
+ `cancelProtection` ni `createProtection`. Entre l'annulation et la nouvelle pose, la position est
385
+ **nue** — et cette fenêtre s'ouvre précisément quand ça va mal : marché rapide, latence réseau,
386
+ serveur qui redémarre. C'est le scénario que tout ce paquet existe pour rendre impossible.
387
+
388
+ Le stop se **déplace** : `moveStop` pose le nouveau **avant** d'annuler l'ancien, du côté du SDK de
389
+ la venue. À aucun instant la position n'est sans protection.
390
+
391
+ ```typescript
392
+ await trading.moveStop(access, { …, stopId: stop.id, triggerPrice: 3120, size: 0.02 }, markPrice);
393
+ ```
394
+
395
+ Si tu te surprends à vouloir annuler puis reposer, c'est un déplacement que tu cherches.
396
+
254
397
  ### Fermer
255
398
 
256
399
  `closePosition` relit la position **sur la venue** avant de la fermer, et sort la taille que la venue
@@ -261,6 +404,39 @@ fill a différé du plan, et ce reliquat reste ouvert, sans stop.
261
404
  await trading.closePosition(access, 'ETH'); // null s'il n'y avait rien à fermer
262
405
  ```
263
406
 
407
+ ### Lire l'état et l'historique
408
+
409
+ ```typescript
410
+ await trading.orderHistory(access, 'ETH'); // les ordres : soumis, remplis, annulés
411
+ await trading.accountInfo(access); // équité, marge, ratio de maintenance (brut)
412
+ await trading.fundingHistory(access, 'ETH', { limit: 100 }); // le funding RÉELLEMENT payé
413
+ trading.newClientOrderId(XgateEx.Hyperliquid); // au format que la venue accepte
414
+ ```
415
+
416
+ **`accountInfo` est unifié** — équité, disponible, marge immobilisée, marge de maintenance, PnL
417
+ latent. Les venues rangent ces valeurs sous des noms sans rapport, mais elles disent la même chose :
418
+
419
+ | | hyperliquid | pacifica | aster |
420
+ |---|---|---|---|
421
+ | équité | `marginSummary.accountValue` | `accountEquity` | `totalMarginBalance` |
422
+ | disponible | `withdrawable` | `availableToSpend` | `availableBalance` |
423
+ | marge utilisée | `marginSummary.totalMarginUsed` | `totalMarginUsed` | `totalInitialMargin` |
424
+ | maintenance | `crossMaintenanceMarginUsed` | `crossMmr` | `totalMaintMargin` |
425
+
426
+ La forme native complète reste dans `xtras` — rien n'est perdu. `unrealizedPnl` vaut `null` là où la
427
+ venue ne le publie pas au niveau du compte (hyperliquid, pacifica) : il se lit alors sur les
428
+ positions. **blofin ne l'expose pas** et lève.
429
+
430
+ **`fundingHistory` dit ce qui a été payé**, là où `PricesService` annonce le taux à venir. Un calcul
431
+ de résultat qui l'ignore surestime toute position tenue longtemps.
432
+
433
+ **`newClientOrderId` n'est pas un `randomUUID()` déguisé** : hyperliquid exige un hexadécimal
434
+ préfixé `0x` (128 bits) quand les autres acceptent un UUID ordinaire. Le mauvais format fait rejeter
435
+ l'ordre.
436
+
437
+ > ⚠️ **`fundingHistory` sur le testnet aster répond `502`** — page HTML, serveur en panne sur cet
438
+ > endpoint. Le mainnet le sert normalement. Le SDK et l'URL sont corrects.
439
+
264
440
  ### Le prix de sortie réel
265
441
 
266
442
  Le prix planifié ne dit pas à quel prix on est sorti : un stop se déclenche au marché, un take-profit