@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 +62 -6
- package/dist/index.cjs +87 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -17
- package/dist/index.d.ts +95 -17
- package/dist/index.js +87 -32
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/index.d.cts
CHANGED
|
@@ -282,6 +282,8 @@ type Unsubscribe = () => void;
|
|
|
282
282
|
*/
|
|
283
283
|
declare class WsCandlesService {
|
|
284
284
|
private readonly logger;
|
|
285
|
+
/** Un client temps réel par venue — voir {@link wireOf}. La clé EST la venue. */
|
|
286
|
+
private readonly wires;
|
|
285
287
|
/**
|
|
286
288
|
* Souscrit aux bougies d'un marché chez une venue. Rend la fonction de désabonnement.
|
|
287
289
|
*
|
|
@@ -308,6 +310,23 @@ declare class WsCandlesService {
|
|
|
308
310
|
symbol: string;
|
|
309
311
|
interval: Timeframe;
|
|
310
312
|
}, handler: (candle: ICandle) => void): Unsubscribe;
|
|
313
|
+
/**
|
|
314
|
+
* Le client temps réel d'une venue — **créé une seule fois, donc UNE SOCKET PAR VENUE**.
|
|
315
|
+
*
|
|
316
|
+
* C'est la règle que `CandlesStreamService` énonce déjà et que ce service violait : chaque
|
|
317
|
+
* `subscribe()` appelait `createPublicXex(xex)`, qui construit une façade NEUVE. Or la façade
|
|
318
|
+
* mémoïse son client temps réel (`unifiedWs()`), et c'est lui qui porte la socket : une façade
|
|
319
|
+
* par souscription, c'est une socket par souscription.
|
|
320
|
+
*
|
|
321
|
+
* Mesuré le 2026-08-13 sur Blips : 1 104 souscriptions ont ouvert **1 104 sockets** sur sept
|
|
322
|
+
* venues, quand hyperliquid n'en accepte que 10 par IP. Les descripteurs s'épuisent, les venues
|
|
323
|
+
* refusent, et chaque connexion refusée rejetait une promesse que personne n'attendait — le
|
|
324
|
+
* process entier mourait deux minutes après l'ouverture des flux. Ici : **sept sockets**.
|
|
325
|
+
*
|
|
326
|
+
* Rien à fermer explicitement : le ref-count vit dans les SDK (`refs += 1` / `release()`), donc
|
|
327
|
+
* la socket se ferme d'elle-même au dernier désabonnement de la venue, et se rouvre au suivant.
|
|
328
|
+
*/
|
|
329
|
+
private wireOf;
|
|
311
330
|
/** Voir `CandlesService.quoteOf` : la cotation se déduit du symbole natif, faute d'être publiée. */
|
|
312
331
|
private quoteOf;
|
|
313
332
|
}
|
|
@@ -340,23 +359,39 @@ declare class CandlesStreamService {
|
|
|
340
359
|
private readonly unsubscribes;
|
|
341
360
|
/** Le plafond de souscriptions documenté par la venue, ou `null` si personne ne l'a publié. */
|
|
342
361
|
readonly maxSubscriptions: number | null;
|
|
362
|
+
/**
|
|
363
|
+
* Notifié quand le flux meurt — après que les souscriptions ont été coupées.
|
|
364
|
+
*
|
|
365
|
+
* **Ce crochet a remplacé un `throw`.** Lever depuis un rappel de socket ne remonte à aucun
|
|
366
|
+
* appelant : l'exception sort en `unhandledRejection` et **tue le process entier**, ce qui est
|
|
367
|
+
* arrivé le 2026-08-13 sur Blips par un autre chemin. « Impossible à ignorer » et « fatal pour
|
|
368
|
+
* tout le monde » ne sont pas la même chose : la première est une garantie, la seconde une panne.
|
|
369
|
+
*
|
|
370
|
+
* Le contrat reste franc — le flux est coupé, il ne repartira pas tout seul, et le silence est
|
|
371
|
+
* journalisé en `.error` de toute façon. Mais c'est à l'appelant de décider ce qu'il en fait :
|
|
372
|
+
* rouvrir, basculer de venue, ou s'arrêter.
|
|
373
|
+
*/
|
|
374
|
+
onFailure: ((error: Error) => void) | null;
|
|
343
375
|
constructor(xex: XgateEx,
|
|
344
376
|
/** Le marché écouté. Le comptant n'est servi que par binance et bybit. */
|
|
345
377
|
kind?: MarketKind);
|
|
346
378
|
/**
|
|
347
379
|
* Ouvre le flux sur une LISTE de marchés, sur une seule socket.
|
|
348
380
|
*
|
|
349
|
-
* **UNE ERREUR DE FLUX COUPE TOUT, PUIS
|
|
381
|
+
* **UNE ERREUR DE FLUX COUPE TOUT, PUIS NOTIFIE.** Elle n'arrive pas à la souscription : mesuré le
|
|
350
382
|
* 2026-08-07, la venue accepte un symbole inconnu sans broncher, puis ferme la connexion en le
|
|
351
383
|
* découvrant. Le SDK reconnecte, elle referme — cinq cycles en douze secondes, et sur une socket
|
|
352
384
|
* partagée **tous** les marchés valides cessent de recevoir, en silence.
|
|
353
385
|
*
|
|
354
386
|
* On refuse cette boucle : à la première erreur, les souscriptions sont coupées, l'incident est
|
|
355
|
-
* journalisé en `error`, et
|
|
356
|
-
* qu'un arrêt franc — on ne découvre le premier qu'en constatant l'absence de
|
|
387
|
+
* journalisé en `error`, et {@link onFailure} est notifié. Un flux à moitié mort qui se tait
|
|
388
|
+
* coûte plus cher qu'un arrêt franc — on ne découvre le premier qu'en constatant l'absence de
|
|
389
|
+
* données.
|
|
357
390
|
*
|
|
358
|
-
* ⚠️
|
|
359
|
-
*
|
|
391
|
+
* ⚠️ **On ne lève pas depuis le rappel de socket.** Une exception née là ne remonte à aucun
|
|
392
|
+
* appelant de `start()` : elle sort en `unhandledRejection` et emporte le process entier — pas
|
|
393
|
+
* seulement le flux fautif, mais les crons et tout ce qui tournait à côté. C'est {@link onFailure}
|
|
394
|
+
* qui porte l'information, et l'appelant qui décide.
|
|
360
395
|
*
|
|
361
396
|
* **binance et bybit ne remontent pas encore leurs erreurs** : leur client gère la fermeture en
|
|
362
397
|
* interne sans l'exposer. Sur ces deux venues, la boucle silencieuse reste possible.
|
|
@@ -969,23 +1004,42 @@ interface IProtection {
|
|
|
969
1004
|
* Ce que l'appelant fournit. **Les pourcentages sont des écarts au prix d'entrée**, en fraction
|
|
970
1005
|
* (0.01 = 1 %) : `slPct: 0.02` place le stop 2 % sous l'entrée en long, 2 % au-dessus en short.
|
|
971
1006
|
*/
|
|
1007
|
+
/**
|
|
1008
|
+
* UN NIVEAU DE SORTIE — dit en ÉCART ou en PRIX, jamais les deux.
|
|
1009
|
+
*
|
|
1010
|
+
* Les deux façons de penser un objectif coexistent réellement : « je sors si ça monte de 10 % »
|
|
1011
|
+
* raisonne en risque, « je sors si ça touche 62 500 » raisonne en niveau technique. Obliger à
|
|
1012
|
+
* convertir l'un dans l'autre revient à faire calculer l'appelant, donc à déplacer l'erreur chez lui.
|
|
1013
|
+
*
|
|
1014
|
+
* L'union est **exclusive** par construction : donner les deux ne compile pas, et n'en donner aucun
|
|
1015
|
+
* non plus. C'est ce qui rend impossible un objectif à moitié défini.
|
|
1016
|
+
*/
|
|
1017
|
+
type ITarget = {
|
|
1018
|
+
pct: number;
|
|
1019
|
+
price?: never;
|
|
1020
|
+
} | {
|
|
1021
|
+
price: number;
|
|
1022
|
+
pct?: never;
|
|
1023
|
+
};
|
|
972
1024
|
interface IProtectionInput {
|
|
973
1025
|
/** Prix d'entrée de référence. */
|
|
974
1026
|
entry: number;
|
|
975
1027
|
direction: Direction;
|
|
976
1028
|
/** Taille de la position, **en unités de base**. */
|
|
977
1029
|
size: number;
|
|
978
|
-
/** Écart du stop au prix d'entrée, en fraction. Toujours du côté perdant. */
|
|
979
|
-
slPct: number;
|
|
980
1030
|
/**
|
|
981
|
-
*
|
|
982
|
-
*
|
|
1031
|
+
* Le stop — en **écart** au prix d'entrée (`{ pct: 0.02 }`) ou en **prix absolu**
|
|
1032
|
+
* (`{ price: 62500 }`). Toujours du côté perdant quand il est donné en écart.
|
|
1033
|
+
*/
|
|
1034
|
+
sl: ITarget;
|
|
1035
|
+
/**
|
|
1036
|
+
* Les take-profits, dans l'ordre. Chacun porte son niveau — en écart ou en prix — et `part`, la
|
|
1037
|
+
* fraction de la POSITION à sortir là (toujours un pourcentage, jamais un prix).
|
|
983
1038
|
*
|
|
984
1039
|
* **Une part à 0 ne produit AUCUN leg** — c'est un marqueur (un palier de breakeven, par exemple),
|
|
985
1040
|
* pas un ordre. Sauf s'il s'agit du dernier : celui-là clôture toujours.
|
|
986
1041
|
*/
|
|
987
|
-
tps: Array<{
|
|
988
|
-
pct: number;
|
|
1042
|
+
tps: Array<ITarget & {
|
|
989
1043
|
part: number;
|
|
990
1044
|
}>;
|
|
991
1045
|
/** Pas de prix de la paire. */
|
|
@@ -1016,6 +1070,12 @@ declare function roundPrice(value: number, tickSize?: string | null, lotSize?: s
|
|
|
1016
1070
|
* L'appelant donne un prix d'entrée, un sens et des écarts ; il ne calcule aucun prix lui-même.
|
|
1017
1071
|
* Le stop part du côté perdant, les take-profits du côté gagnant — le sens s'en occupe.
|
|
1018
1072
|
*/
|
|
1073
|
+
/**
|
|
1074
|
+
* Les niveaux depuis des POURCENTAGES seuls — forme courte quand tout est exprimé en écart.
|
|
1075
|
+
*
|
|
1076
|
+
* {@link buildProtection} ne l'utilise plus : il résout chaque niveau séparément, puisqu'un stop
|
|
1077
|
+
* peut être donné en écart et un take-profit en prix absolu dans le même appel.
|
|
1078
|
+
*/
|
|
1019
1079
|
declare function buildLevels(entry: number, direction: Direction, slPct: number, tpPcts: readonly number[]): {
|
|
1020
1080
|
sl: number;
|
|
1021
1081
|
tps: number[];
|
|
@@ -1261,16 +1321,34 @@ interface IEntryWithProtection {
|
|
|
1261
1321
|
size: number;
|
|
1262
1322
|
/** Prix d'entrée visé. Sert de référence aux pourcentages, et de prix limite. */
|
|
1263
1323
|
entry: number;
|
|
1264
|
-
/** Écart du stop au prix d'entrée, en fraction (0.02 = 2 %). Obligatoire. */
|
|
1265
|
-
slPct: number;
|
|
1266
1324
|
/**
|
|
1267
|
-
*
|
|
1325
|
+
* LE STOP — en **écart** au prix d'entrée ou en **prix absolu**, jamais les deux.
|
|
1326
|
+
*
|
|
1327
|
+
* ```typescript
|
|
1328
|
+
* sl: { pct: 0.02 } // 2 % sous l'entrée en long, 2 % au-dessus en short
|
|
1329
|
+
* sl: { price: 62500 } // exactement 62 500, quel que soit le prix d'entrée
|
|
1330
|
+
* ```
|
|
1331
|
+
*
|
|
1332
|
+
* **Obligatoire.** C'est la règle qui a motivé tout ce chemin : jamais un ordre sans stop.
|
|
1333
|
+
*/
|
|
1334
|
+
sl: ITarget;
|
|
1335
|
+
/**
|
|
1336
|
+
* LES TAKE-PROFITS, dans l'ordre. Chacun dit son NIVEAU et sa PART.
|
|
1337
|
+
*
|
|
1338
|
+
* ```typescript
|
|
1339
|
+
* tps: [{ pct: 0.03, part: 0.5 }, // sortir 50 % si ça monte de 3 %
|
|
1340
|
+
* { price: 62500, part: 0.5 }] // sortir 50 % si ça touche 62 500
|
|
1341
|
+
* ```
|
|
1342
|
+
*
|
|
1343
|
+
* **`part` est toujours une fraction de la POSITION**, jamais un prix : les deux modes ne portent
|
|
1344
|
+
* que sur le niveau. Un objectif se pense soit en risque (« +3 % »), soit en niveau technique
|
|
1345
|
+
* (« 62 500 ») — obliger à convertir l'un dans l'autre déplacerait le calcul, donc l'erreur, chez
|
|
1346
|
+
* l'appelant.
|
|
1268
1347
|
*
|
|
1269
1348
|
* Au-dessus de 80 % cumulés, le dernier absorbe le reliquat et la position ferme entièrement ;
|
|
1270
1349
|
* à 80 % ou en dessous, le reste court.
|
|
1271
1350
|
*/
|
|
1272
|
-
tps: Array<{
|
|
1273
|
-
pct: number;
|
|
1351
|
+
tps: Array<ITarget & {
|
|
1274
1352
|
part: number;
|
|
1275
1353
|
}>;
|
|
1276
1354
|
/** `ioc` pour entrer au marché (taker), `alo` pour poster (maker). Défaut : `ioc`. */
|
|
@@ -1651,4 +1729,4 @@ declare function expiryOf(xtras?: Record<string, unknown>): Date | undefined;
|
|
|
1651
1729
|
*/
|
|
1652
1730
|
declare function isDated(xtras?: Record<string, unknown>): boolean;
|
|
1653
1731
|
|
|
1654
|
-
export { CandlesService, CandlesStreamRegistry, CandlesStreamService, CatalogService, type Direction, type IAccountState, type IBalance, type ICandle, type ICandlesQuery, type IEntryWithProtection, type IEquityPoint, type IFundingPaid, type IMoveStop, type IMovement, type IOrder, type IPair, type IPosition, type IPrice, type IProtection, type IProtectionInput, type IProtectionLeg, type IPublicTrade, type ITrade, type ITradingAccess, type IWalletAccess, type IXexAccess, type IXexSpotSource, type MarketKind, type MovementKind, type MovementStatus, ORDER_STATUSES, type OrderStatus, type OrderType, PricesService, SPOT_XEXES, type Side, SpotCandlesService, SpotCatalogService, SpotPricesService, SpotWsCandlesService, TIMEFRAME_MINUTES, type TimeInForce, Timeframe, TradingService, type Unsubscribe, WALLET_XEXES, WS_SUBSCRIPTION_LIMITS, WalletService, WsCandlesService, WsTradesService, XGATE_EXCHANGES, XgateEx, XgateModule, buildLevels, buildProtection, canonicalFromBase, canonicalFromXex, expiryOf, isDated, multiplierFromXex, roundPrice, roundToStep, servesNatively, servesSpot, sourceFor, subscriptionLimitOf, timeframeMs };
|
|
1732
|
+
export { CandlesService, CandlesStreamRegistry, CandlesStreamService, CatalogService, type Direction, type IAccountState, type IBalance, type ICandle, type ICandlesQuery, type IEntryWithProtection, type IEquityPoint, type IFundingPaid, type IMoveStop, type IMovement, type IOrder, type IPair, type IPosition, type IPrice, type IProtection, type IProtectionInput, type IProtectionLeg, type IPublicTrade, type ITarget, type ITrade, type ITradingAccess, type IWalletAccess, type IXexAccess, type IXexSpotSource, type MarketKind, type MovementKind, type MovementStatus, ORDER_STATUSES, type OrderStatus, type OrderType, PricesService, SPOT_XEXES, type Side, SpotCandlesService, SpotCatalogService, SpotPricesService, SpotWsCandlesService, TIMEFRAME_MINUTES, type TimeInForce, Timeframe, TradingService, type Unsubscribe, WALLET_XEXES, WS_SUBSCRIPTION_LIMITS, WalletService, WsCandlesService, WsTradesService, XGATE_EXCHANGES, XgateEx, XgateModule, buildLevels, buildProtection, canonicalFromBase, canonicalFromXex, expiryOf, isDated, multiplierFromXex, roundPrice, roundToStep, servesNatively, servesSpot, sourceFor, subscriptionLimitOf, timeframeMs };
|
package/dist/index.d.ts
CHANGED
|
@@ -282,6 +282,8 @@ type Unsubscribe = () => void;
|
|
|
282
282
|
*/
|
|
283
283
|
declare class WsCandlesService {
|
|
284
284
|
private readonly logger;
|
|
285
|
+
/** Un client temps réel par venue — voir {@link wireOf}. La clé EST la venue. */
|
|
286
|
+
private readonly wires;
|
|
285
287
|
/**
|
|
286
288
|
* Souscrit aux bougies d'un marché chez une venue. Rend la fonction de désabonnement.
|
|
287
289
|
*
|
|
@@ -308,6 +310,23 @@ declare class WsCandlesService {
|
|
|
308
310
|
symbol: string;
|
|
309
311
|
interval: Timeframe;
|
|
310
312
|
}, handler: (candle: ICandle) => void): Unsubscribe;
|
|
313
|
+
/**
|
|
314
|
+
* Le client temps réel d'une venue — **créé une seule fois, donc UNE SOCKET PAR VENUE**.
|
|
315
|
+
*
|
|
316
|
+
* C'est la règle que `CandlesStreamService` énonce déjà et que ce service violait : chaque
|
|
317
|
+
* `subscribe()` appelait `createPublicXex(xex)`, qui construit une façade NEUVE. Or la façade
|
|
318
|
+
* mémoïse son client temps réel (`unifiedWs()`), et c'est lui qui porte la socket : une façade
|
|
319
|
+
* par souscription, c'est une socket par souscription.
|
|
320
|
+
*
|
|
321
|
+
* Mesuré le 2026-08-13 sur Blips : 1 104 souscriptions ont ouvert **1 104 sockets** sur sept
|
|
322
|
+
* venues, quand hyperliquid n'en accepte que 10 par IP. Les descripteurs s'épuisent, les venues
|
|
323
|
+
* refusent, et chaque connexion refusée rejetait une promesse que personne n'attendait — le
|
|
324
|
+
* process entier mourait deux minutes après l'ouverture des flux. Ici : **sept sockets**.
|
|
325
|
+
*
|
|
326
|
+
* Rien à fermer explicitement : le ref-count vit dans les SDK (`refs += 1` / `release()`), donc
|
|
327
|
+
* la socket se ferme d'elle-même au dernier désabonnement de la venue, et se rouvre au suivant.
|
|
328
|
+
*/
|
|
329
|
+
private wireOf;
|
|
311
330
|
/** Voir `CandlesService.quoteOf` : la cotation se déduit du symbole natif, faute d'être publiée. */
|
|
312
331
|
private quoteOf;
|
|
313
332
|
}
|
|
@@ -340,23 +359,39 @@ declare class CandlesStreamService {
|
|
|
340
359
|
private readonly unsubscribes;
|
|
341
360
|
/** Le plafond de souscriptions documenté par la venue, ou `null` si personne ne l'a publié. */
|
|
342
361
|
readonly maxSubscriptions: number | null;
|
|
362
|
+
/**
|
|
363
|
+
* Notifié quand le flux meurt — après que les souscriptions ont été coupées.
|
|
364
|
+
*
|
|
365
|
+
* **Ce crochet a remplacé un `throw`.** Lever depuis un rappel de socket ne remonte à aucun
|
|
366
|
+
* appelant : l'exception sort en `unhandledRejection` et **tue le process entier**, ce qui est
|
|
367
|
+
* arrivé le 2026-08-13 sur Blips par un autre chemin. « Impossible à ignorer » et « fatal pour
|
|
368
|
+
* tout le monde » ne sont pas la même chose : la première est une garantie, la seconde une panne.
|
|
369
|
+
*
|
|
370
|
+
* Le contrat reste franc — le flux est coupé, il ne repartira pas tout seul, et le silence est
|
|
371
|
+
* journalisé en `.error` de toute façon. Mais c'est à l'appelant de décider ce qu'il en fait :
|
|
372
|
+
* rouvrir, basculer de venue, ou s'arrêter.
|
|
373
|
+
*/
|
|
374
|
+
onFailure: ((error: Error) => void) | null;
|
|
343
375
|
constructor(xex: XgateEx,
|
|
344
376
|
/** Le marché écouté. Le comptant n'est servi que par binance et bybit. */
|
|
345
377
|
kind?: MarketKind);
|
|
346
378
|
/**
|
|
347
379
|
* Ouvre le flux sur une LISTE de marchés, sur une seule socket.
|
|
348
380
|
*
|
|
349
|
-
* **UNE ERREUR DE FLUX COUPE TOUT, PUIS
|
|
381
|
+
* **UNE ERREUR DE FLUX COUPE TOUT, PUIS NOTIFIE.** Elle n'arrive pas à la souscription : mesuré le
|
|
350
382
|
* 2026-08-07, la venue accepte un symbole inconnu sans broncher, puis ferme la connexion en le
|
|
351
383
|
* découvrant. Le SDK reconnecte, elle referme — cinq cycles en douze secondes, et sur une socket
|
|
352
384
|
* partagée **tous** les marchés valides cessent de recevoir, en silence.
|
|
353
385
|
*
|
|
354
386
|
* On refuse cette boucle : à la première erreur, les souscriptions sont coupées, l'incident est
|
|
355
|
-
* journalisé en `error`, et
|
|
356
|
-
* qu'un arrêt franc — on ne découvre le premier qu'en constatant l'absence de
|
|
387
|
+
* journalisé en `error`, et {@link onFailure} est notifié. Un flux à moitié mort qui se tait
|
|
388
|
+
* coûte plus cher qu'un arrêt franc — on ne découvre le premier qu'en constatant l'absence de
|
|
389
|
+
* données.
|
|
357
390
|
*
|
|
358
|
-
* ⚠️
|
|
359
|
-
*
|
|
391
|
+
* ⚠️ **On ne lève pas depuis le rappel de socket.** Une exception née là ne remonte à aucun
|
|
392
|
+
* appelant de `start()` : elle sort en `unhandledRejection` et emporte le process entier — pas
|
|
393
|
+
* seulement le flux fautif, mais les crons et tout ce qui tournait à côté. C'est {@link onFailure}
|
|
394
|
+
* qui porte l'information, et l'appelant qui décide.
|
|
360
395
|
*
|
|
361
396
|
* **binance et bybit ne remontent pas encore leurs erreurs** : leur client gère la fermeture en
|
|
362
397
|
* interne sans l'exposer. Sur ces deux venues, la boucle silencieuse reste possible.
|
|
@@ -969,23 +1004,42 @@ interface IProtection {
|
|
|
969
1004
|
* Ce que l'appelant fournit. **Les pourcentages sont des écarts au prix d'entrée**, en fraction
|
|
970
1005
|
* (0.01 = 1 %) : `slPct: 0.02` place le stop 2 % sous l'entrée en long, 2 % au-dessus en short.
|
|
971
1006
|
*/
|
|
1007
|
+
/**
|
|
1008
|
+
* UN NIVEAU DE SORTIE — dit en ÉCART ou en PRIX, jamais les deux.
|
|
1009
|
+
*
|
|
1010
|
+
* Les deux façons de penser un objectif coexistent réellement : « je sors si ça monte de 10 % »
|
|
1011
|
+
* raisonne en risque, « je sors si ça touche 62 500 » raisonne en niveau technique. Obliger à
|
|
1012
|
+
* convertir l'un dans l'autre revient à faire calculer l'appelant, donc à déplacer l'erreur chez lui.
|
|
1013
|
+
*
|
|
1014
|
+
* L'union est **exclusive** par construction : donner les deux ne compile pas, et n'en donner aucun
|
|
1015
|
+
* non plus. C'est ce qui rend impossible un objectif à moitié défini.
|
|
1016
|
+
*/
|
|
1017
|
+
type ITarget = {
|
|
1018
|
+
pct: number;
|
|
1019
|
+
price?: never;
|
|
1020
|
+
} | {
|
|
1021
|
+
price: number;
|
|
1022
|
+
pct?: never;
|
|
1023
|
+
};
|
|
972
1024
|
interface IProtectionInput {
|
|
973
1025
|
/** Prix d'entrée de référence. */
|
|
974
1026
|
entry: number;
|
|
975
1027
|
direction: Direction;
|
|
976
1028
|
/** Taille de la position, **en unités de base**. */
|
|
977
1029
|
size: number;
|
|
978
|
-
/** Écart du stop au prix d'entrée, en fraction. Toujours du côté perdant. */
|
|
979
|
-
slPct: number;
|
|
980
1030
|
/**
|
|
981
|
-
*
|
|
982
|
-
*
|
|
1031
|
+
* Le stop — en **écart** au prix d'entrée (`{ pct: 0.02 }`) ou en **prix absolu**
|
|
1032
|
+
* (`{ price: 62500 }`). Toujours du côté perdant quand il est donné en écart.
|
|
1033
|
+
*/
|
|
1034
|
+
sl: ITarget;
|
|
1035
|
+
/**
|
|
1036
|
+
* Les take-profits, dans l'ordre. Chacun porte son niveau — en écart ou en prix — et `part`, la
|
|
1037
|
+
* fraction de la POSITION à sortir là (toujours un pourcentage, jamais un prix).
|
|
983
1038
|
*
|
|
984
1039
|
* **Une part à 0 ne produit AUCUN leg** — c'est un marqueur (un palier de breakeven, par exemple),
|
|
985
1040
|
* pas un ordre. Sauf s'il s'agit du dernier : celui-là clôture toujours.
|
|
986
1041
|
*/
|
|
987
|
-
tps: Array<{
|
|
988
|
-
pct: number;
|
|
1042
|
+
tps: Array<ITarget & {
|
|
989
1043
|
part: number;
|
|
990
1044
|
}>;
|
|
991
1045
|
/** Pas de prix de la paire. */
|
|
@@ -1016,6 +1070,12 @@ declare function roundPrice(value: number, tickSize?: string | null, lotSize?: s
|
|
|
1016
1070
|
* L'appelant donne un prix d'entrée, un sens et des écarts ; il ne calcule aucun prix lui-même.
|
|
1017
1071
|
* Le stop part du côté perdant, les take-profits du côté gagnant — le sens s'en occupe.
|
|
1018
1072
|
*/
|
|
1073
|
+
/**
|
|
1074
|
+
* Les niveaux depuis des POURCENTAGES seuls — forme courte quand tout est exprimé en écart.
|
|
1075
|
+
*
|
|
1076
|
+
* {@link buildProtection} ne l'utilise plus : il résout chaque niveau séparément, puisqu'un stop
|
|
1077
|
+
* peut être donné en écart et un take-profit en prix absolu dans le même appel.
|
|
1078
|
+
*/
|
|
1019
1079
|
declare function buildLevels(entry: number, direction: Direction, slPct: number, tpPcts: readonly number[]): {
|
|
1020
1080
|
sl: number;
|
|
1021
1081
|
tps: number[];
|
|
@@ -1261,16 +1321,34 @@ interface IEntryWithProtection {
|
|
|
1261
1321
|
size: number;
|
|
1262
1322
|
/** Prix d'entrée visé. Sert de référence aux pourcentages, et de prix limite. */
|
|
1263
1323
|
entry: number;
|
|
1264
|
-
/** Écart du stop au prix d'entrée, en fraction (0.02 = 2 %). Obligatoire. */
|
|
1265
|
-
slPct: number;
|
|
1266
1324
|
/**
|
|
1267
|
-
*
|
|
1325
|
+
* LE STOP — en **écart** au prix d'entrée ou en **prix absolu**, jamais les deux.
|
|
1326
|
+
*
|
|
1327
|
+
* ```typescript
|
|
1328
|
+
* sl: { pct: 0.02 } // 2 % sous l'entrée en long, 2 % au-dessus en short
|
|
1329
|
+
* sl: { price: 62500 } // exactement 62 500, quel que soit le prix d'entrée
|
|
1330
|
+
* ```
|
|
1331
|
+
*
|
|
1332
|
+
* **Obligatoire.** C'est la règle qui a motivé tout ce chemin : jamais un ordre sans stop.
|
|
1333
|
+
*/
|
|
1334
|
+
sl: ITarget;
|
|
1335
|
+
/**
|
|
1336
|
+
* LES TAKE-PROFITS, dans l'ordre. Chacun dit son NIVEAU et sa PART.
|
|
1337
|
+
*
|
|
1338
|
+
* ```typescript
|
|
1339
|
+
* tps: [{ pct: 0.03, part: 0.5 }, // sortir 50 % si ça monte de 3 %
|
|
1340
|
+
* { price: 62500, part: 0.5 }] // sortir 50 % si ça touche 62 500
|
|
1341
|
+
* ```
|
|
1342
|
+
*
|
|
1343
|
+
* **`part` est toujours une fraction de la POSITION**, jamais un prix : les deux modes ne portent
|
|
1344
|
+
* que sur le niveau. Un objectif se pense soit en risque (« +3 % »), soit en niveau technique
|
|
1345
|
+
* (« 62 500 ») — obliger à convertir l'un dans l'autre déplacerait le calcul, donc l'erreur, chez
|
|
1346
|
+
* l'appelant.
|
|
1268
1347
|
*
|
|
1269
1348
|
* Au-dessus de 80 % cumulés, le dernier absorbe le reliquat et la position ferme entièrement ;
|
|
1270
1349
|
* à 80 % ou en dessous, le reste court.
|
|
1271
1350
|
*/
|
|
1272
|
-
tps: Array<{
|
|
1273
|
-
pct: number;
|
|
1351
|
+
tps: Array<ITarget & {
|
|
1274
1352
|
part: number;
|
|
1275
1353
|
}>;
|
|
1276
1354
|
/** `ioc` pour entrer au marché (taker), `alo` pour poster (maker). Défaut : `ioc`. */
|
|
@@ -1651,4 +1729,4 @@ declare function expiryOf(xtras?: Record<string, unknown>): Date | undefined;
|
|
|
1651
1729
|
*/
|
|
1652
1730
|
declare function isDated(xtras?: Record<string, unknown>): boolean;
|
|
1653
1731
|
|
|
1654
|
-
export { CandlesService, CandlesStreamRegistry, CandlesStreamService, CatalogService, type Direction, type IAccountState, type IBalance, type ICandle, type ICandlesQuery, type IEntryWithProtection, type IEquityPoint, type IFundingPaid, type IMoveStop, type IMovement, type IOrder, type IPair, type IPosition, type IPrice, type IProtection, type IProtectionInput, type IProtectionLeg, type IPublicTrade, type ITrade, type ITradingAccess, type IWalletAccess, type IXexAccess, type IXexSpotSource, type MarketKind, type MovementKind, type MovementStatus, ORDER_STATUSES, type OrderStatus, type OrderType, PricesService, SPOT_XEXES, type Side, SpotCandlesService, SpotCatalogService, SpotPricesService, SpotWsCandlesService, TIMEFRAME_MINUTES, type TimeInForce, Timeframe, TradingService, type Unsubscribe, WALLET_XEXES, WS_SUBSCRIPTION_LIMITS, WalletService, WsCandlesService, WsTradesService, XGATE_EXCHANGES, XgateEx, XgateModule, buildLevels, buildProtection, canonicalFromBase, canonicalFromXex, expiryOf, isDated, multiplierFromXex, roundPrice, roundToStep, servesNatively, servesSpot, sourceFor, subscriptionLimitOf, timeframeMs };
|
|
1732
|
+
export { CandlesService, CandlesStreamRegistry, CandlesStreamService, CatalogService, type Direction, type IAccountState, type IBalance, type ICandle, type ICandlesQuery, type IEntryWithProtection, type IEquityPoint, type IFundingPaid, type IMoveStop, type IMovement, type IOrder, type IPair, type IPosition, type IPrice, type IProtection, type IProtectionInput, type IProtectionLeg, type IPublicTrade, type ITarget, type ITrade, type ITradingAccess, type IWalletAccess, type IXexAccess, type IXexSpotSource, type MarketKind, type MovementKind, type MovementStatus, ORDER_STATUSES, type OrderStatus, type OrderType, PricesService, SPOT_XEXES, type Side, SpotCandlesService, SpotCatalogService, SpotPricesService, SpotWsCandlesService, TIMEFRAME_MINUTES, type TimeInForce, Timeframe, TradingService, type Unsubscribe, WALLET_XEXES, WS_SUBSCRIPTION_LIMITS, WalletService, WsCandlesService, WsTradesService, XGATE_EXCHANGES, XgateEx, XgateModule, buildLevels, buildProtection, canonicalFromBase, canonicalFromXex, expiryOf, isDated, multiplierFromXex, roundPrice, roundToStep, servesNatively, servesSpot, sourceFor, subscriptionLimitOf, timeframeMs };
|
package/dist/index.js
CHANGED
|
@@ -95,6 +95,24 @@ function toCandle(wire, xex, interval, quote) {
|
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
// src/helpers/socket-error.ts
|
|
99
|
+
function lisible(error) {
|
|
100
|
+
if (error instanceof Error) {
|
|
101
|
+
return error.message;
|
|
102
|
+
}
|
|
103
|
+
const event = error;
|
|
104
|
+
if (event !== null && typeof event === "object") {
|
|
105
|
+
const message = event.message ?? event.error?.message;
|
|
106
|
+
if (typeof message === "string" && message !== "") {
|
|
107
|
+
return message;
|
|
108
|
+
}
|
|
109
|
+
if (typeof event.type === "string" && event.type !== "") {
|
|
110
|
+
return `\xE9v\xE9nement \xAB ${event.type} \xBB`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return String(error);
|
|
114
|
+
}
|
|
115
|
+
|
|
98
116
|
// src/helpers/ws-limits.ts
|
|
99
117
|
var WS_SUBSCRIPTION_LIMITS = {
|
|
100
118
|
aster: 200,
|
|
@@ -264,20 +282,36 @@ var CandlesStreamService = class {
|
|
|
264
282
|
unsubscribes = [];
|
|
265
283
|
/** Le plafond de souscriptions documenté par la venue, ou `null` si personne ne l'a publié. */
|
|
266
284
|
maxSubscriptions;
|
|
285
|
+
/**
|
|
286
|
+
* Notifié quand le flux meurt — après que les souscriptions ont été coupées.
|
|
287
|
+
*
|
|
288
|
+
* **Ce crochet a remplacé un `throw`.** Lever depuis un rappel de socket ne remonte à aucun
|
|
289
|
+
* appelant : l'exception sort en `unhandledRejection` et **tue le process entier**, ce qui est
|
|
290
|
+
* arrivé le 2026-08-13 sur Blips par un autre chemin. « Impossible à ignorer » et « fatal pour
|
|
291
|
+
* tout le monde » ne sont pas la même chose : la première est une garantie, la seconde une panne.
|
|
292
|
+
*
|
|
293
|
+
* Le contrat reste franc — le flux est coupé, il ne repartira pas tout seul, et le silence est
|
|
294
|
+
* journalisé en `.error` de toute façon. Mais c'est à l'appelant de décider ce qu'il en fait :
|
|
295
|
+
* rouvrir, basculer de venue, ou s'arrêter.
|
|
296
|
+
*/
|
|
297
|
+
onFailure = null;
|
|
267
298
|
/**
|
|
268
299
|
* Ouvre le flux sur une LISTE de marchés, sur une seule socket.
|
|
269
300
|
*
|
|
270
|
-
* **UNE ERREUR DE FLUX COUPE TOUT, PUIS
|
|
301
|
+
* **UNE ERREUR DE FLUX COUPE TOUT, PUIS NOTIFIE.** Elle n'arrive pas à la souscription : mesuré le
|
|
271
302
|
* 2026-08-07, la venue accepte un symbole inconnu sans broncher, puis ferme la connexion en le
|
|
272
303
|
* découvrant. Le SDK reconnecte, elle referme — cinq cycles en douze secondes, et sur une socket
|
|
273
304
|
* partagée **tous** les marchés valides cessent de recevoir, en silence.
|
|
274
305
|
*
|
|
275
306
|
* On refuse cette boucle : à la première erreur, les souscriptions sont coupées, l'incident est
|
|
276
|
-
* journalisé en `error`, et
|
|
277
|
-
* qu'un arrêt franc — on ne découvre le premier qu'en constatant l'absence de
|
|
307
|
+
* journalisé en `error`, et {@link onFailure} est notifié. Un flux à moitié mort qui se tait
|
|
308
|
+
* coûte plus cher qu'un arrêt franc — on ne découvre le premier qu'en constatant l'absence de
|
|
309
|
+
* données.
|
|
278
310
|
*
|
|
279
|
-
* ⚠️
|
|
280
|
-
*
|
|
311
|
+
* ⚠️ **On ne lève pas depuis le rappel de socket.** Une exception née là ne remonte à aucun
|
|
312
|
+
* appelant de `start()` : elle sort en `unhandledRejection` et emporte le process entier — pas
|
|
313
|
+
* seulement le flux fautif, mais les crons et tout ce qui tournait à côté. C'est {@link onFailure}
|
|
314
|
+
* qui porte l'information, et l'appelant qui décide.
|
|
281
315
|
*
|
|
282
316
|
* **binance et bybit ne remontent pas encore leurs erreurs** : leur client gère la fermeture en
|
|
283
317
|
* interne sans l'exposer. Sur ces deux venues, la boucle silencieuse reste possible.
|
|
@@ -289,7 +323,7 @@ var CandlesStreamService = class {
|
|
|
289
323
|
const raison = lisible(error);
|
|
290
324
|
this.logger.error(`${this.xex} : erreur de flux \u2014 ${raison}. Souscriptions coup\xE9es.`);
|
|
291
325
|
this.stop();
|
|
292
|
-
|
|
326
|
+
this.onFailure?.(new Error(`stream(${this.xex}) : flux interrompu \u2014 ${raison}`));
|
|
293
327
|
};
|
|
294
328
|
}
|
|
295
329
|
for (const symbolXex of symbolsXex) {
|
|
@@ -372,22 +406,6 @@ var CandlesStreamRegistry = class {
|
|
|
372
406
|
CandlesStreamRegistry = __decorateClass([
|
|
373
407
|
Injectable()
|
|
374
408
|
], CandlesStreamRegistry);
|
|
375
|
-
function lisible(error) {
|
|
376
|
-
if (error instanceof Error) {
|
|
377
|
-
return error.message;
|
|
378
|
-
}
|
|
379
|
-
const event = error;
|
|
380
|
-
if (event !== null && typeof event === "object") {
|
|
381
|
-
const message = event.message ?? event.error?.message;
|
|
382
|
-
if (typeof message === "string" && message !== "") {
|
|
383
|
-
return message;
|
|
384
|
-
}
|
|
385
|
-
if (typeof event.type === "string" && event.type !== "") {
|
|
386
|
-
return `\xE9v\xE9nement \xAB ${event.type} \xBB`;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
return String(error);
|
|
390
|
-
}
|
|
391
409
|
|
|
392
410
|
// src/enums/timeframe.enums.ts
|
|
393
411
|
var Timeframe = /* @__PURE__ */ ((Timeframe2) => {
|
|
@@ -1075,6 +1093,13 @@ SpotWsCandlesService = __decorateClass([
|
|
|
1075
1093
|
], SpotWsCandlesService);
|
|
1076
1094
|
|
|
1077
1095
|
// src/helpers/protection.ts
|
|
1096
|
+
function priceOf(cible, entry, direction, gagnant) {
|
|
1097
|
+
if (cible.price !== void 0) {
|
|
1098
|
+
return round8(cible.price);
|
|
1099
|
+
}
|
|
1100
|
+
const dir = (direction === "long" ? 1 : -1) * (gagnant === true ? 1 : -1);
|
|
1101
|
+
return round8(entry * (1 + dir * cible.pct));
|
|
1102
|
+
}
|
|
1078
1103
|
var SLIPPAGE_DEFAUT = 5e-3;
|
|
1079
1104
|
var SEUIL_CLOTURE = 0.8;
|
|
1080
1105
|
function round8(value) {
|
|
@@ -1117,12 +1142,8 @@ function buildLevels(entry, direction, slPct, tpPcts) {
|
|
|
1117
1142
|
function buildProtection(input) {
|
|
1118
1143
|
const slippage = input.slippagePct ?? SLIPPAGE_DEFAUT;
|
|
1119
1144
|
const long = input.direction === "long";
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
input.direction,
|
|
1123
|
-
input.slPct,
|
|
1124
|
-
input.tps.map((tp) => tp.pct)
|
|
1125
|
-
);
|
|
1145
|
+
const sl = priceOf(input.sl, input.entry, input.direction, false);
|
|
1146
|
+
const niveaux = input.tps.map((tp) => priceOf(tp, input.entry, input.direction, true));
|
|
1126
1147
|
const borne = (trigger) => long === true ? trigger * (1 - slippage) : trigger * (1 + slippage);
|
|
1127
1148
|
const sommeParts = input.tps.reduce((total, tp) => total + tp.part, 0);
|
|
1128
1149
|
const clotureIntegrale = sommeParts > SEUIL_CLOTURE;
|
|
@@ -1193,7 +1214,7 @@ var TradingService = class {
|
|
|
1193
1214
|
entry: input.entry,
|
|
1194
1215
|
direction: input.direction,
|
|
1195
1216
|
size: input.size,
|
|
1196
|
-
|
|
1217
|
+
sl: input.sl,
|
|
1197
1218
|
tps: input.tps,
|
|
1198
1219
|
tickSize: input.tickSize,
|
|
1199
1220
|
lotSize: input.lotSize,
|
|
@@ -1816,6 +1837,8 @@ WalletService = __decorateClass([
|
|
|
1816
1837
|
], WalletService);
|
|
1817
1838
|
var WsCandlesService = class {
|
|
1818
1839
|
logger = new Logger(WsCandlesService.name);
|
|
1840
|
+
/** Un client temps réel par venue — voir {@link wireOf}. La clé EST la venue. */
|
|
1841
|
+
wires = /* @__PURE__ */ new Map();
|
|
1819
1842
|
/**
|
|
1820
1843
|
* Souscrit aux bougies d'un marché chez une venue. Rend la fonction de désabonnement.
|
|
1821
1844
|
*
|
|
@@ -1831,9 +1854,12 @@ var WsCandlesService = class {
|
|
|
1831
1854
|
subscribe(xex, query, handler) {
|
|
1832
1855
|
const quote = this.quoteOf(query.symbol);
|
|
1833
1856
|
this.logger.log(`souscription bougies ${query.symbol} ${query.interval} sur ${xex}`);
|
|
1834
|
-
return
|
|
1835
|
-
|
|
1836
|
-
|
|
1857
|
+
return this.wireOf(xex).subscribeCandles(
|
|
1858
|
+
{ name: query.symbol, interval: query.interval },
|
|
1859
|
+
(wire) => {
|
|
1860
|
+
handler(toCandle(wire, xex, query.interval, quote));
|
|
1861
|
+
}
|
|
1862
|
+
);
|
|
1837
1863
|
}
|
|
1838
1864
|
/**
|
|
1839
1865
|
* Souscrit au MÊME marché chez plusieurs venues, avec un seul handler.
|
|
@@ -1860,6 +1886,35 @@ var WsCandlesService = class {
|
|
|
1860
1886
|
}
|
|
1861
1887
|
};
|
|
1862
1888
|
}
|
|
1889
|
+
/**
|
|
1890
|
+
* Le client temps réel d'une venue — **créé une seule fois, donc UNE SOCKET PAR VENUE**.
|
|
1891
|
+
*
|
|
1892
|
+
* C'est la règle que `CandlesStreamService` énonce déjà et que ce service violait : chaque
|
|
1893
|
+
* `subscribe()` appelait `createPublicXex(xex)`, qui construit une façade NEUVE. Or la façade
|
|
1894
|
+
* mémoïse son client temps réel (`unifiedWs()`), et c'est lui qui porte la socket : une façade
|
|
1895
|
+
* par souscription, c'est une socket par souscription.
|
|
1896
|
+
*
|
|
1897
|
+
* Mesuré le 2026-08-13 sur Blips : 1 104 souscriptions ont ouvert **1 104 sockets** sur sept
|
|
1898
|
+
* venues, quand hyperliquid n'en accepte que 10 par IP. Les descripteurs s'épuisent, les venues
|
|
1899
|
+
* refusent, et chaque connexion refusée rejetait une promesse que personne n'attendait — le
|
|
1900
|
+
* process entier mourait deux minutes après l'ouverture des flux. Ici : **sept sockets**.
|
|
1901
|
+
*
|
|
1902
|
+
* Rien à fermer explicitement : le ref-count vit dans les SDK (`refs += 1` / `release()`), donc
|
|
1903
|
+
* la socket se ferme d'elle-même au dernier désabonnement de la venue, et se rouvre au suivant.
|
|
1904
|
+
*/
|
|
1905
|
+
wireOf(xex) {
|
|
1906
|
+
let wire = this.wires.get(xex);
|
|
1907
|
+
if (wire === void 0) {
|
|
1908
|
+
wire = createPublicXex(xex).ws();
|
|
1909
|
+
if ("onError" in wire) {
|
|
1910
|
+
wire.onError = (error) => {
|
|
1911
|
+
this.logger.error(`${xex} : erreur de flux \u2014 ${lisible(error)}`);
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
this.wires.set(xex, wire);
|
|
1915
|
+
}
|
|
1916
|
+
return wire;
|
|
1917
|
+
}
|
|
1863
1918
|
/** Voir `CandlesService.quoteOf` : la cotation se déduit du symbole natif, faute d'être publiée. */
|
|
1864
1919
|
quoteOf(symbolXex) {
|
|
1865
1920
|
const parts = symbolXex.split("-");
|