@blackcube/xgate-sdk 0.22.0 → 0.24.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,24 +22,37 @@ 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 | ✔ | — | — | — |
29
29
  | bullet | ✔ *(bougies en WS seulement)* | — | — | — |
30
30
  | blofin | ✔ | ✔ | ✔ | — |
31
- | binance | ✔ *(+ comptant)* | — | — | — |
32
- | bybit | ✔ *(+ comptant)* | — | — | — |
31
+ | binance | ✔ **+ comptant complet** | — | — | — |
32
+ | bybit | ✔ **+ comptant complet** | — | — | — |
33
33
 
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,
@@ -53,29 +66,129 @@ bougies passent donc par `WsCandlesService` ; `CandlesService` y lève.
53
66
 
54
67
  **Les autres sont en lecture.** Elles servent le catalogue, les bougies et les prix ; il leur manque
55
68
  un accès de compte (clé d'API pour bullet, adresse active pour extended et paradex) ou la partie
56
- signée n'est pas câblée. Binance et bybit sont volontairement limitées au **public** : leurs
57
- SDK ne couvrent que catalogue, bougies et prix, et toute route signée y lève.
69
+ signée n'est pas câblée. Binance et bybit sont volontairement limitées au **public**, mais y servent les deux marchés :
70
+ perpétuel ET comptant, chacun avec ses quatre services. Toute route signée y lève.
58
71
 
59
72
  ## Les services
60
73
 
61
74
  Chacun est un `@Injectable()` NestJS, utilisable seul ou via `XgateModule`.
62
75
 
76
+ **Les marchés se lisent par paires de services** : le nom de la classe dit lequel on interroge.
77
+
78
+ | Ce qu'on lit | Perpétuel — 10 venues | Comptant — binance, bybit |
79
+ |---|---|---|
80
+ | catalogue | `CatalogService` | `SpotCatalogService` |
81
+ | bougies REST | `CandlesService` | `SpotCandlesService` |
82
+ | prix | `PricesService` | `SpotPricesService` |
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.
89
+
90
+ `CandlesService` agrège quand la venue ne sert pas l'intervalle demandé ; les autres servent ce que
91
+ la venue publie. Un `Spot*` appelé sur une venue sans comptant **lève**.
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
+
98
+ ### ⚠️ Les sockets réagissent MAL à un symbole invalide
99
+
100
+ **À lire avant d'ouvrir un flux.** Aucune des venues ne rejette proprement un marché inconnu : elles
101
+ l'acceptent, puis se comportent mal — chacune à sa façon, et deux d'entre elles **en silence**.
102
+
103
+ | venue | ce qu'elle fait d'un symbole inconnu | XGate le voit ? |
104
+ |---|---|---|
105
+ | hyperliquid | ferme la connexion et reconnecte **en boucle** | ✔ coupe tout et lève |
106
+ | binance | l'ignore, les autres marchés continuent | sans objet |
107
+ | **bybit** | **fait taire TOUT le flux**, socket ouverte, sans erreur | ✘ **indétectable** |
108
+
109
+ Le cas bybit est le plus grave : `['BTCUSDT', 'INCONNU']` ne rend **aucune** bougie — pas même pour
110
+ `BTCUSDT` — alors que `['BTCUSDT', 'ETHUSDT']` fonctionne. Pas de fermeture, pas d'erreur, pas de
111
+ reconnexion. C'est un **silence**, pas un incident : aucun canal ne peut le remonter, et rien dans
112
+ XGate ne le détectera jamais.
113
+
114
+ Le cas hyperliquid était le même silence avant que XGate ne coupe : cinq reconnexions en douze
115
+ secondes pendant lesquelles les marchés **valides** du même socket se taisaient aussi.
116
+
117
+ **La seule protection : souscrire depuis le catalogue**, jamais depuis une liste écrite à la main.
118
+
119
+ ```typescript
120
+ const paires = await catalog.catalog(XgateEx.Bybit);
121
+ const symboles = paires.filter((p) => /* ta sélection */).map((p) => p.symbolXex);
122
+ registry.of(XgateEx.Bybit).start(symboles, Timeframe.M1, (candle) => { … });
123
+ ```
124
+
125
+ ### Suivre plusieurs marchés : `CandlesStreamRegistry`
126
+
127
+ **Une socket par venue, quel que soit le nombre de marchés.** Porté de Blips, où il tourne en
128
+ production.
129
+
130
+ ```typescript
131
+ const flux = registry.of(XgateEx.Hyperliquid);
132
+ flux.start(['BTC', 'ETH', 'SOL'], Timeframe.M1, (candle) => { … });
133
+ flux.subscriptionCount(); // 3
134
+ flux.maxSubscriptions; // 1000 — plafond documenté, null si la venue n'en publie pas
135
+ registry.stopAll();
136
+ ```
137
+
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.
141
+
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` |
154
+
155
+ > **Une erreur de flux coupe tout, puis lève.** Un symbole invalide n'échoue pas à la
156
+ > souscription : la venue l'accepte, puis ferme la connexion en le découvrant — cinq cycles de
157
+ > reconnexion en douze secondes, mesurés le 2026-08-07 sur hyperliquid, pendant lesquels **tous**
158
+ > les marchés valides du même socket cessent de recevoir, en silence.
159
+ >
160
+ > À la première erreur, XGate coupe les souscriptions, journalise en `error`, et lève. L'exception
161
+ > naît dans un rappel de socket : elle ne remonte pas à l'appelant de `start()`, elle sort en
162
+ > erreur non capturée — délibérément, pour être impossible à ignorer.
163
+ >
164
+ > Souscris depuis le catalogue, pas depuis une liste écrite à la main.
165
+ >
166
+ > **Les trois venues ne réagissent pas pareil à un symbole invalide** — mesuré le 2026-08-07 :
167
+ >
168
+ > | venue | ce qu'elle fait | détecté ? |
169
+ > |---|---|---|
170
+ > | hyperliquid | ferme la connexion, reconnecte en boucle | ✔ coupe et lève |
171
+ > | binance | l'ignore, les autres marchés continuent | sans objet |
172
+ > | **bybit** | **tout le flux se tait**, sans fermeture ni erreur | ✘ **rien ne le signale** |
173
+ >
174
+ > ⚠️ **bybit est le cas dangereux** : `['BTCUSDT', 'INCONNU']` ne rend **aucune** bougie, pas même
175
+ > pour `BTCUSDT`, et la socket reste ouverte sans erreur. Aucun canal ne peut le détecter — c'est
176
+ > un silence, pas un incident. La seule protection est de **souscrire depuis le catalogue**.
177
+
178
+ **Deux services sans équivalent comptant**, parce que binance et bybit sont bornées au public :
179
+
63
180
  | Service | Ce qu'il rend |
64
181
  |---|---|
65
- | `CatalogService` | les paires perpétuelles de toutes les venues, normalisées |
66
- | `SpotCatalogService` | les paires **comptant** — binance et bybit uniquement |
67
- | `CandlesService` | les bougies REST **perpétuelles**, avec agrégation quand la venue ne sert pas l'intervalle |
68
- | `SpotCandlesService` | les bougies REST **comptant** — binance et bybit |
69
- | `WsCandlesService` | les bougies en temps réel, un handler pour N venues |
70
- | `PricesService` | mark, oracle, mid, bid/ask, funding, open interest |
71
182
  | `WalletService` | soldes, mouvements, historique d'équité |
72
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** |
73
185
 
74
186
  Le cycle de vie d'une position tient en trois gestes, et chacun préserve le même invariant : la
75
187
  position n'est jamais sans stop.
76
188
 
77
189
  | geste | méthode | venues |
78
190
  |---|---|---|
191
+ | régler le levier | `setLeverage` | toutes sauf bullet |
79
192
  | ouvrir avec sa protection | `openWithProtection` | hyperliquid, pacifica |
80
193
  | déplacer le stop | `moveStop` | hyperliquid, pacifica, aster |
81
194
  | fermer sans reliquat | `closePosition` | toutes celles qui tradent |
@@ -83,6 +196,16 @@ position n'est jamais sans stop.
83
196
 
84
197
  ## Le portefeuille
85
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
+
86
209
  `balances()` rend **le comptant et le collatéral**, chaque ligne portant sa nature :
87
210
 
88
211
  ```typescript
@@ -172,6 +295,89 @@ l'erreur ne se voit qu'une fois l'argent parti. Le prix courant est donc un argu
172
295
  > une chaîne nue, sans identifiant — pour un stop groupé sous son entrée. Il se retrouve au carnet,
173
296
  > où `reduceOnly === true` le distingue.
174
297
 
298
+ ### Savoir ce qui s'est rempli : `WsTradesService`
299
+
300
+ Le flux des exécutions du compte, en temps réel. C'est ce qui permet de connaître son état **exact**
301
+ sans sonder : entre deux interrogations, un take-profit partiel a pu changer la taille de la
302
+ position, et toute décision prise sur l'ancienne valeur est fausse.
303
+
304
+ ```typescript
305
+ const stop = wsTrades.subscribe(access, (trade) => {
306
+ // trade.price est le prix RÉELLEMENT obtenu, pas celui demandé
307
+ });
308
+ ```
309
+
310
+ **Seules les exécutions survenues après l'abonnement sont transmises.** Les venues rejouent leur
311
+ historique à la souscription — mesuré sur hyperliquid : 30 exécutions dans les six premières
312
+ secondes, toutes antérieures, la plus ancienne de plus de deux jours. XGate les écarte, sans quoi
313
+ chaque reconnexion rejouerait deux jours de trades et ferait compter deux fois les mêmes
314
+ remplissages. Pour l'historique, `TradingService.trades()` est fait pour ça.
315
+
316
+ `subscribeAll([...accès], handler)` suit plusieurs comptes avec un seul handler. **blofin ne diffuse
317
+ pas ses exécutions** et lève ; les sept autres venues les servent.
318
+
319
+ **Trois flux, trois usages** :
320
+
321
+ ```typescript
322
+ wsTrades.subscribe(access, (trade) => { … }); // MES exécutions : à quel prix
323
+ wsTrades.subscribeOrders(access, (order) => { … }); // MES ordres : leur cycle de vie
324
+ wsTrades.subscribePublicTrades(access, 'ETH', (trade) => { … }); // le marché : ce qui se négocie
325
+ ```
326
+
327
+ `subscribeOrders` voit l'ordre **naître et changer d'état** (`open` → `filled`, ou `canceled`) —
328
+ c'est là qu'on apprend qu'un stop vient de se déclencher. `subscribe` dit à quel prix. Les deux
329
+ écartent le rejeu d'historique ; `subscribePublicTrades` n'en a pas à écarter.
330
+
331
+ ### La marge : mode, renfort, allègement
332
+
333
+ ```typescript
334
+ await trading.setMarginMode(access, 'ETH', true); // true = isolée, false = croisée
335
+ await trading.addMargin(access, 'ETH', '2'); // éloigne la liquidation
336
+ await trading.removeMargin(access, 'ETH', '1'); // libère du capital
337
+ ```
338
+
339
+ **Le mode se règle AVANT d'ouvrir.** En **croisée**, tout le solde garantit la position : elle tient
340
+ plus longtemps, mais une liquidation emporte l'ensemble. En **isolée**, seule la marge affectée est
341
+ en jeu : la perte est bornée, la liquidation arrive plus tôt.
342
+
343
+ `addMargin` renforce une position sous pression **sans la réduire** — vérifié en réel : marge 38,45
344
+ → 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
345
+ garantit déjà la position.
346
+
347
+ ⚠️ **pacifica ne retire pas de marge** et `removeMargin` y lève : fermer partiellement la position
348
+ est alors la façon de libérer du capital.
349
+
350
+ ### Le coupe-circuit : `cancelAll`
351
+
352
+ Annule **tous** les ordres d'une paire d'un geste — pour reprendre la main sans discuter : état
353
+ incohérent, arrêt d'urgence, reprise après incident. Annuler un par un laisse une fenêtre pendant
354
+ laquelle certains vivent encore.
355
+
356
+ ```typescript
357
+ const annules = await trading.cancelAll(access, 'ETH'); // nombre annulé, ou null
358
+ ```
359
+
360
+ ⚠️ **Cela retire aussi les protections.** Stops et take-profits sont des ordres comme les autres :
361
+ une position ouverte se retrouve **nue** après ce geste. XGate le journalise en `warn` s'il détecte
362
+ une position, mais ne refuse pas — c'est un coupe-circuit, il doit couper. À n'employer que si la
363
+ position est fermée, ou en reposant une protection dans la foulée.
364
+
365
+ ### Ne jamais annuler pour reposer
366
+
367
+ **Annuler une protection pour la recréer est une aberration**, et XGate n'expose délibérément ni
368
+ `cancelProtection` ni `createProtection`. Entre l'annulation et la nouvelle pose, la position est
369
+ **nue** — et cette fenêtre s'ouvre précisément quand ça va mal : marché rapide, latence réseau,
370
+ serveur qui redémarre. C'est le scénario que tout ce paquet existe pour rendre impossible.
371
+
372
+ Le stop se **déplace** : `moveStop` pose le nouveau **avant** d'annuler l'ancien, du côté du SDK de
373
+ la venue. À aucun instant la position n'est sans protection.
374
+
375
+ ```typescript
376
+ await trading.moveStop(access, { …, stopId: stop.id, triggerPrice: 3120, size: 0.02 }, markPrice);
377
+ ```
378
+
379
+ Si tu te surprends à vouloir annuler puis reposer, c'est un déplacement que tu cherches.
380
+
175
381
  ### Fermer
176
382
 
177
383
  `closePosition` relit la position **sur la venue** avant de la fermer, et sort la taille que la venue
@@ -182,6 +388,39 @@ fill a différé du plan, et ce reliquat reste ouvert, sans stop.
182
388
  await trading.closePosition(access, 'ETH'); // null s'il n'y avait rien à fermer
183
389
  ```
184
390
 
391
+ ### Lire l'état et l'historique
392
+
393
+ ```typescript
394
+ await trading.orderHistory(access, 'ETH'); // les ordres : soumis, remplis, annulés
395
+ await trading.accountInfo(access); // équité, marge, ratio de maintenance (brut)
396
+ await trading.fundingHistory(access, 'ETH', { limit: 100 }); // le funding RÉELLEMENT payé
397
+ trading.newClientOrderId(XgateEx.Hyperliquid); // au format que la venue accepte
398
+ ```
399
+
400
+ **`accountInfo` est unifié** — équité, disponible, marge immobilisée, marge de maintenance, PnL
401
+ latent. Les venues rangent ces valeurs sous des noms sans rapport, mais elles disent la même chose :
402
+
403
+ | | hyperliquid | pacifica | aster |
404
+ |---|---|---|---|
405
+ | équité | `marginSummary.accountValue` | `accountEquity` | `totalMarginBalance` |
406
+ | disponible | `withdrawable` | `availableToSpend` | `availableBalance` |
407
+ | marge utilisée | `marginSummary.totalMarginUsed` | `totalMarginUsed` | `totalInitialMargin` |
408
+ | maintenance | `crossMaintenanceMarginUsed` | `crossMmr` | `totalMaintMargin` |
409
+
410
+ La forme native complète reste dans `xtras` — rien n'est perdu. `unrealizedPnl` vaut `null` là où la
411
+ venue ne le publie pas au niveau du compte (hyperliquid, pacifica) : il se lit alors sur les
412
+ positions. **blofin ne l'expose pas** et lève.
413
+
414
+ **`fundingHistory` dit ce qui a été payé**, là où `PricesService` annonce le taux à venir. Un calcul
415
+ de résultat qui l'ignore surestime toute position tenue longtemps.
416
+
417
+ **`newClientOrderId` n'est pas un `randomUUID()` déguisé** : hyperliquid exige un hexadécimal
418
+ préfixé `0x` (128 bits) quand les autres acceptent un UUID ordinaire. Le mauvais format fait rejeter
419
+ l'ordre.
420
+
421
+ > ⚠️ **`fundingHistory` sur le testnet aster répond `502`** — page HTML, serveur en panne sur cet
422
+ > endpoint. Le mainnet le sert normalement. Le SDK et l'URL sont corrects.
423
+
185
424
  ### Le prix de sortie réel
186
425
 
187
426
  Le prix planifié ne dit pas à quel prix on est sorti : un stop se déclenche au marché, un take-profit
@@ -215,19 +454,50 @@ open · partiallyFilled · filled · canceled · rejected · expired · other
215
454
  `canceled` prend **un seul L**. Un statut inconnu retombe sur `other` plutôt que de traverser la
216
455
  passerelle tel quel : `ORDER_STATUSES` est exporté pour valider plutôt que caster.
217
456
 
218
- ## Comptant et perpétuel ne se mélangent pas
457
+ ## Le comptant, au même niveau que le perpétuel
458
+
459
+ **Quatre services de chaque côté**, jamais un drapeau. Le nom de la classe dit quel marché on lit.
219
460
 
220
- Deux services, jamais un drapeau : `CandlesService` sert le perpétuel, `SpotCandlesService` le
221
- comptant.
461
+ | | perpétuel | comptant |
462
+ |---|---|---|
463
+ | catalogue | `CatalogService` | `SpotCatalogService` |
464
+ | bougies REST | `CandlesService` | `SpotCandlesService` |
465
+ | prix | `PricesService` | `SpotPricesService` |
466
+ | temps réel | `WsCandlesService` | `SpotWsCandlesService` |
222
467
 
223
468
  ```typescript
469
+ await spotCatalog.catalog(XgateEx.Binance, XgateEx.Bybit);
224
470
  await spotCandles.candles(XgateEx.Binance, { symbol: 'BTCUSDT', interval: '1h', limit: 100 });
225
- await spotCandles.candlesOf([XgateEx.Binance, XgateEx.Bybit], { symbol: 'BTCUSDT', interval: '1h' });
471
+ await spotPrices.prices(XgateEx.Binance, XgateEx.Bybit);
472
+ spotWs.subscribe(XgateEx.Binance, { symbol: 'BTCUSDT', interval: '1m' }, (candle) => { … });
226
473
  ```
227
474
 
228
- Ce n'est pas de la coquetterie d'API : **les échelles de cotation diffèrent entre les deux
229
- marchés**. `SATSUSDT` au comptant chez bybit cote ×1 quand son perpétuel cote ×10000 — mélanger les
230
- deux séries fabrique un graphe qui saute d'un facteur 10 000. Une venue sans comptant lève.
475
+ Le comptant est servi par **binance et bybit** ; toute autre venue lève.
476
+
477
+ **Pourquoi deux jeux de services et non un paramètre** : ce sont deux marchés, pas deux vues du
478
+ même. Le perpétuel porte un funding et s'écarte du comptant — c'est cet écart que certaines
479
+ stratégies exploitent, et le confondre le rendrait invisible. Un `candles(xex, { spot: true })`
480
+ laisserait croire qu'on regarde la même chose sous un autre angle.
481
+
482
+ **Ce qui change concrètement quand on passe au comptant :**
483
+
484
+ - **La cotation n'est plus toujours un dollar.** En perpétuel, tout est en USDT/USDC. Au comptant,
485
+ 495 paires sont cotées en BTC, 375 en BNB, 370 en TRY, 230 en ETH — le champ `quote` devient
486
+ indispensable, et le « prix » d'`ETHBTC` est un ratio, pas des dollars.
487
+ - **`mark`, `oracle`, `funding` et `openInterest` valent `null`** : ils n'existent que sur un
488
+ contrat. Les voir renseignés signalerait qu'on lit en réalité du perpétuel.
489
+
490
+ > Les échelles de cotation, elles, ne posent **aucun** problème entre les deux marchés :
491
+ > `SATSUSDT` (×1) et `10000SATSUSDT` (×10000) sont **deux paires distinctes**, chacune portant son
492
+ > `multiplier` au catalogue. Une bougie ou un prix appartient à une paire, identifiée par
493
+ > `xex` + `symbolXex` + `kind` — il n'y a rien à réconcilier.
494
+
495
+ **419 actifs n'existent qu'au comptant** — le perpétuel en couvre 887, le comptant 962, dont 419
496
+ sans aucun perpétuel nulle part.
497
+
498
+ > Sur `SpotPricesService`, `symbol` est dérivé du symbole concaténé : `BTCUSDT` → `BTC`. Les paires
499
+ > cotées hors dollar gardent leur nom entier (`ETHBTC`), les prix ne portant pas la base déclarée.
500
+ > Pour un `symbol`/`quote` justes sur ces paires, passer par `SpotCatalogService`.
231
501
 
232
502
  ## La semaine s'ouvre le lundi, partout
233
503
 
@@ -267,6 +537,56 @@ L'échéance est dérivée du **type de contrat**, jamais de la date de livraiso
267
537
  `deliveryDate: 4133404800000` — le 1ᵉʳ janvier 2100 — sur **tous** ses perpétuels. S'y fier
268
538
  marquerait le catalogue entier comme daté.
269
539
 
540
+ ## Les actifs non-crypto du catalogue
541
+
542
+ **Extraction du 2026-08-07, et rien de plus.** Cette liste n'est pas un contrat : elle a été
543
+ relevée un jour donné sur les dix venues, et elle bougera au prochain listing sans que ce fichier
544
+ le sache. Ne l'utilise pas comme référence figée — refais l'extraction quand la question compte.
545
+
546
+ 164 actifs du catalogue perpétuel ne sont pas de la crypto. Ils s'y mélangent sans distinction :
547
+ un balayage qui ramène « les paires BTC-like » y ramasse de l'or, du pétrole et des actions Tesla,
548
+ alors que **ni les horaires de cotation, ni la volatilité, ni la dynamique de funding** ne s'y
549
+ comportent pareil — les actions et indices ne cotent pas 24/7.
550
+
551
+ **forex (7)**
552
+ `AUDUSD EURUSD GBPUSD NZDUSD USDCAD USDCHF USDJPY`
553
+
554
+ **matières premières (11)**
555
+ `BZ CL COPPER GOLD NG SILVER WTIOIL XAG XAU XPD XPT`
556
+
557
+ **indices et ETF (36)** — dont des ETF à effet de levier (`SOXL` ×3, `SQQQ` inverse ×3, `UVXY`)
558
+ `BBX BITO BNC BOT BSP EWJ EWT EWY EWZ FWDI INTW IWM KORU KSTR MUU MVLL QNTX QQQ SHAZ SMH SNXX SOXL
559
+ SOXS SP500 SPY SQQQ STXX TBT TMF TQQQ TZA URNM US100 UVXY XBI XLE`
560
+
561
+ **hors-US et pré-IPO (29)** — `OPENAI`, `ANTHROPIC`, `SPCX`, `ZHIPU`, `MINIMAX` sont des sociétés
562
+ **non cotées** : des marchés sur valorisation privée
563
+ `AAOI ANTHROPIC ARM ASML AXTI BABA BMNR CBRS CRWV DRAM GIGADEV HK0700 HK1810 HYUNDAI MINIMAX NBIS
564
+ NVO OPENAI PENG POPMART SAMSUNG SKHY SKHYNIX SONY SPCX TENCENT TSM USAR ZHIPU`
565
+
566
+ **actions US (81)**
567
+ `AAPL ADBE ALAB AMAT AMD AMZN APP ASTS AVGO BE BRKB BX CAT CIEN COHR COIN COST CRCL CRDO CRM CRWD
568
+ CSCO DELL DIS DKNG EBAY FLEX FLNC GEV GLW GME GOOGL GS HD HIMS HOOD HPE IBM INTC IREN JPM KLAC KO
569
+ LITE LLY LRCX META MRVL MSFT MSTR MU NFLX NOK NOW NVDA ONDS ORCL PANW PAYP PLTR PYPL QCOM RDDT
570
+ RIVN RKLB SMCI SNDK SNOW SOFI STRC TER TSLA TTWO TXN UBER V VRT WDC WEN WMT ZM`
571
+
572
+ ### Ce que cette liste vaut, et ce qu'elle ne vaut pas
573
+
574
+ **Deux venues seulement les déclarent** : extended (`contractType: 'TRADIFI_PERPETUAL'`, 153
575
+ marchés) et bullet (`RwaPerpUsEquity`, `RwaPerpCommodities`, `RwaPerpKrEquity`,
576
+ `RwaPerpUsEquityIndices`, 10 marchés). Les **huit autres n'en disent rien** — bybit en cote 131,
577
+ aster 108, blofin 74, lighter 66, pacifica 20, paradex 19, sans aucun champ pour les distinguer.
578
+ La liste ne tient donc que parce qu'un actif déclaré quelque part se reconnaît ailleurs à son
579
+ symbole canonique.
580
+
581
+ **Le sous-classement est une interprétation**, pas une donnée de venue. Le forex se reconnaît à sa
582
+ forme (six lettres, deux codes ISO) ; le reste vient d'un rangement fait à la main sur les tickers.
583
+ Ce qui est solide, c'est **l'appartenance à la liste** — pas la catégorie.
584
+
585
+ **Aucun champ `assetType` n'existe dans `IPair`**, et c'est délibéré tant qu'aucune source
586
+ d'autorité ne couvre les dix venues. La documentation officielle de pacifica le confirme : son
587
+ `/api/v1/info` ne publie que `instrument_type`, qui vaut `perpetual` aussi bien pour `BTC` que pour
588
+ `TSLA`, `XAU` ou `EURUSD`.
589
+
270
590
  ## Les conventions qui traversent tout
271
591
 
272
592
  - **Une bougie inclut sa dernière milliseconde** : 10:00:00.000 → 10:59:59.999. `closedAt` se