@syldel/hl-shared-types 0.0.16 → 0.0.18

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
@@ -16,6 +16,33 @@ Ce package centralise les définitions TypeScript pour l'écosystème **Hyperliq
16
16
  * **Account** : États du compte, positions Perp et soldes Spot.
17
17
  * **Market** : Métadonnées des actifs et résumés de marché.
18
18
  * **Orders** : Définitions des ordres ouverts et historiques.
19
+ * **Format** : les règles de **tick et de lot** d'Hyperliquid — la seule logique exécutable
20
+ du package, et la seule qui décide ce qui part vers l'exchange.
21
+
22
+ ---
23
+
24
+ ## ⚠️ `format/` : du code, pas seulement des types
25
+
26
+ `formatPrice`, `formatSize`, `priceDecimals` et `snapPrice` appliquent les règles de
27
+ [tick and lot size](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/tick-and-lot-size)
28
+ d'Hyperliquid. Elles vivent ici pour qu'il n'en existe **qu'une seule implémentation** :
29
+ le gateway les applique en sortie, et le bot doit pouvoir savoir *avant d'envoyer* ce qui
30
+ sera posé. Deux implémentations, c'est la garantie qu'un émetteur croira un jour avoir posé
31
+ autre chose que ce qui l'a été — ce qui s'est produit, et se paie sur un stop loss.
32
+
33
+ Trois conséquences pour qui touche à ce dossier :
34
+
35
+ * **aucune dépendance**, ici moins qu'ailleurs : le consommateur de ce code est le processus
36
+ qui signe les transactions ;
37
+ * **rien ne passe par `Number`** — tout le calcul se fait sur les chiffres écrits, en
38
+ `BigInt` ;
39
+ * **la conformité se prouve** : `test/tick-and-lot.spec.ts` rejoue *tous* les exemples de la
40
+ documentation, sourcés et datés. `npm run build` lance les tests avant de compiler — une
41
+ logique exécutable non testée ne se publie pas.
42
+
43
+ ```bash
44
+ npm test
45
+ ```
19
46
 
20
47
  ---
21
48
 
@@ -0,0 +1 @@
1
+ export * from './tick-and-lot';
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./tick-and-lot"), exports);
@@ -0,0 +1,89 @@
1
+ /**
2
+ * ============================================================================
3
+ * PRIX ET TAILLES : LES RÈGLES DE TICK ET DE LOT D'HYPERLIQUID
4
+ *
5
+ * Source : https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/tick-and-lot-size
6
+ * relevée le 2026-09-23. Deux règles, et une exception :
7
+ *
8
+ * - **taille** : tronquée à `szDecimals`, propre à chaque actif (réponse `meta`) ;
9
+ * - **prix** : au plus 5 chiffres significatifs, ET au plus
10
+ * `MAX_DECIMALS - szDecimals` décimales, `MAX_DECIMALS` valant 6 en perp et
11
+ * 8 en spot ;
12
+ * - **un prix entier est toujours accepté**, quel que soit son nombre de
13
+ * chiffres significatifs (`123456` est valide là où `12345.6` ne l'est pas).
14
+ *
15
+ * Ce module vit ici, et non dans le gateway ou le bot, pour qu'il n'en existe
16
+ * **qu'une seule implémentation** : un prix mal écrit est refusé par
17
+ * Hyperliquid, ou accepté sous une forme tronquée qui n'est plus celle qu'on
18
+ * voulait. Sur un stop loss, la différence entre les deux, c'est une position
19
+ * protégée et une position qui ne l'est pas. Deux implémentations, c'est la
20
+ * garantie qu'un émetteur croira un jour avoir posé autre chose que ce qui
21
+ * l'a été.
22
+ *
23
+ * ⚠️ Un faux gateway de test, lui, doit garder sa **copie indépendante** : s'il
24
+ * importait ces fonctions, un défaut s'y annulerait des deux côtés et aucun
25
+ * test ne le verrait.
26
+ *
27
+ * Rien ne passe par `Number` : tout le calcul se fait sur les chiffres écrits,
28
+ * en `BigInt`. Un `parseFloat` réintroduirait exactement l'erreur d'arrondi que
29
+ * ces règles servent à éviter.
30
+ * ============================================================================
31
+ */
32
+ /** Marché visé : les deux n'ont pas le même plafond de décimales. */
33
+ export type HLMarketType = 'perp' | 'spot';
34
+ /**
35
+ * Sens de l'arrondi. `truncate` va vers zéro — c'est ce que fait Hyperliquid,
36
+ * et le seul sens autorisé pour une taille. `up` et `down` vont vers +∞ et
37
+ * −∞, ce qui ne diffère de `truncate` que sur une valeur négative.
38
+ */
39
+ export type RoundingMode = 'truncate' | 'up' | 'down' | 'nearest';
40
+ /** Levée plutôt que de rendre une valeur que l'exchange refuserait. */
41
+ export declare class TickAndLotError extends Error {
42
+ constructor(message: string);
43
+ }
44
+ /** `MAX_DECIMALS` de la documentation, par type de marché. */
45
+ export declare const MAX_PRICE_DECIMALS: Readonly<Record<HLMarketType, number>>;
46
+ /** Le plafond de chiffres significatifs d'un prix non entier. */
47
+ export declare const MAX_PRICE_SIGNIFICANT_DIGITS = 5;
48
+ /**
49
+ * Arrondit une valeur à `decimals` décimales, dans le sens demandé, sans
50
+ * jamais passer par un flottant.
51
+ */
52
+ export declare function roundDecimal(value: string | number, decimals: number, mode?: RoundingMode): string;
53
+ /**
54
+ * Le nombre de décimales que ce prix a le droit d'avoir — les deux règles
55
+ * réunies en un seul nombre.
56
+ *
57
+ * Le plancher à zéro **est** l'exception des entiers : quand les chiffres
58
+ * significatifs en réclameraient moins que zéro décimale (un prix au-delà de
59
+ * 99 999), la documentation autorise malgré tout n'importe quel entier, donc
60
+ * on s'arrête à l'entier plutôt que de raboter aussi les unités.
61
+ */
62
+ export declare function priceDecimals(price: string | number, szDecimals: number, type?: HLMarketType): number;
63
+ /**
64
+ * Le prix, réécrit comme Hyperliquid l'accepte : tronqué à ce que
65
+ * `priceDecimals` autorise.
66
+ *
67
+ * @throws {TickAndLotError} si la valeur est illisible, ou tronquée à zéro.
68
+ */
69
+ export declare function formatPrice(price: string | number, szDecimals: number, type?: HLMarketType): string;
70
+ /**
71
+ * La taille, réécrite comme Hyperliquid l'accepte : **tronquée** à
72
+ * `szDecimals`, jamais arrondie. Arrondir vers le haut, ce serait trader plus
73
+ * que ce qui a été calculé — dépasser une position sur un ordre `reduceOnly`,
74
+ * ou le collatéral sur une ouverture.
75
+ *
76
+ * @throws {TickAndLotError} si la valeur est illisible, ou tronquée à zéro.
77
+ */
78
+ export declare function formatSize(size: string | number, szDecimals: number): string;
79
+ /**
80
+ * Pose un prix sur la grille de l'actif **dans un sens choisi**, et garantit
81
+ * que le résultat en est un point fixe : `formatPrice(snapPrice(p)) === snapPrice(p)`.
82
+ *
83
+ * C'est ce que `formatPrice` seul ne sait pas faire — il tronque toujours.
84
+ * Un émetteur qui veut qu'un stop ne s'éloigne jamais de son ancre a besoin du
85
+ * sens opposé, et il a besoin que le formatage final ne défasse pas ce choix.
86
+ *
87
+ * @throws {TickAndLotError} si la valeur est illisible, ou ramenée à zéro.
88
+ */
89
+ export declare function snapPrice(price: string | number, szDecimals: number, type?: HLMarketType, mode?: RoundingMode): string;
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+ /**
3
+ * ============================================================================
4
+ * PRIX ET TAILLES : LES RÈGLES DE TICK ET DE LOT D'HYPERLIQUID
5
+ *
6
+ * Source : https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/tick-and-lot-size
7
+ * relevée le 2026-09-23. Deux règles, et une exception :
8
+ *
9
+ * - **taille** : tronquée à `szDecimals`, propre à chaque actif (réponse `meta`) ;
10
+ * - **prix** : au plus 5 chiffres significatifs, ET au plus
11
+ * `MAX_DECIMALS - szDecimals` décimales, `MAX_DECIMALS` valant 6 en perp et
12
+ * 8 en spot ;
13
+ * - **un prix entier est toujours accepté**, quel que soit son nombre de
14
+ * chiffres significatifs (`123456` est valide là où `12345.6` ne l'est pas).
15
+ *
16
+ * Ce module vit ici, et non dans le gateway ou le bot, pour qu'il n'en existe
17
+ * **qu'une seule implémentation** : un prix mal écrit est refusé par
18
+ * Hyperliquid, ou accepté sous une forme tronquée qui n'est plus celle qu'on
19
+ * voulait. Sur un stop loss, la différence entre les deux, c'est une position
20
+ * protégée et une position qui ne l'est pas. Deux implémentations, c'est la
21
+ * garantie qu'un émetteur croira un jour avoir posé autre chose que ce qui
22
+ * l'a été.
23
+ *
24
+ * ⚠️ Un faux gateway de test, lui, doit garder sa **copie indépendante** : s'il
25
+ * importait ces fonctions, un défaut s'y annulerait des deux côtés et aucun
26
+ * test ne le verrait.
27
+ *
28
+ * Rien ne passe par `Number` : tout le calcul se fait sur les chiffres écrits,
29
+ * en `BigInt`. Un `parseFloat` réintroduirait exactement l'erreur d'arrondi que
30
+ * ces règles servent à éviter.
31
+ * ============================================================================
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.MAX_PRICE_SIGNIFICANT_DIGITS = exports.MAX_PRICE_DECIMALS = exports.TickAndLotError = void 0;
35
+ exports.roundDecimal = roundDecimal;
36
+ exports.priceDecimals = priceDecimals;
37
+ exports.formatPrice = formatPrice;
38
+ exports.formatSize = formatSize;
39
+ exports.snapPrice = snapPrice;
40
+ /** Levée plutôt que de rendre une valeur que l'exchange refuserait. */
41
+ class TickAndLotError extends Error {
42
+ constructor(message) {
43
+ super(message);
44
+ this.name = 'TickAndLotError';
45
+ }
46
+ }
47
+ exports.TickAndLotError = TickAndLotError;
48
+ /** `MAX_DECIMALS` de la documentation, par type de marché. */
49
+ exports.MAX_PRICE_DECIMALS = {
50
+ perp: 6,
51
+ spot: 8,
52
+ };
53
+ /** Le plafond de chiffres significatifs d'un prix non entier. */
54
+ exports.MAX_PRICE_SIGNIFICANT_DIGITS = 5;
55
+ /** Un décimal simple, signe optionnel : `12`, `-0.5`, `.75`, `3.`. */
56
+ const PLAIN_DECIMAL = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
57
+ const EXPONENTIAL = /^([+-]?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)$/;
58
+ /**
59
+ * Réécrit une notation scientifique en décimal simple. `String(0.0000001)`
60
+ * rend `'1e-7'`, que la documentation n'accepte pas et que nous refuserions
61
+ * plus loin : autant l'écrire correctement ici.
62
+ */
63
+ function expandExponential(text) {
64
+ const match = EXPONENTIAL.exec(text);
65
+ if (!match)
66
+ return text;
67
+ const [, sign, int, frac = '', exponentText] = match;
68
+ const digits = `${int}${frac}`;
69
+ const pointIndex = int.length + Number(exponentText);
70
+ if (pointIndex <= 0) {
71
+ return `${sign}0.${'0'.repeat(-pointIndex)}${digits}`;
72
+ }
73
+ if (pointIndex >= digits.length) {
74
+ return `${sign}${digits}${'0'.repeat(pointIndex - digits.length)}`;
75
+ }
76
+ return `${sign}${digits.slice(0, pointIndex)}.${digits.slice(pointIndex)}`;
77
+ }
78
+ /**
79
+ * Rend le texte décimal d'une valeur.
80
+ *
81
+ * Un `number` est une valeur **calculée** : on l'écrit exactement, notation
82
+ * scientifique dépliée. Une `string` est un texte qui vient déjà d'ailleurs —
83
+ * d'un DTO, de l'API — et doit donc être un décimal simple : une notation
84
+ * scientifique y signale qu'un flottant s'est glissé en amont, et la laisser
85
+ * passer masquerait le défaut.
86
+ */
87
+ function toDecimalText(value, field) {
88
+ if (typeof value === 'number') {
89
+ if (!Number.isFinite(value)) {
90
+ throw new TickAndLotError(`${field} is not finite: ${String(value)}`);
91
+ }
92
+ return expandExponential(String(value));
93
+ }
94
+ const text = String(value).trim();
95
+ if (!PLAIN_DECIMAL.test(text)) {
96
+ throw new TickAndLotError(`${field} is not a plain decimal string: ${JSON.stringify(value)}`);
97
+ }
98
+ return text;
99
+ }
100
+ function split(text) {
101
+ const negative = text.startsWith('-');
102
+ const [int = '', frac = ''] = text.replace(/^[+-]/, '').split('.');
103
+ return { negative, int: int || '0', frac };
104
+ }
105
+ /** Réécrit une valeur mise à l'échelle, sans zéro de queue ni `-0`. */
106
+ function formatUnits(negative, units, decimals) {
107
+ const digits = units.toString().padStart(decimals + 1, '0');
108
+ const int = digits.slice(0, digits.length - decimals);
109
+ const frac = decimals > 0 ? digits.slice(digits.length - decimals).replace(/0+$/, '') : '';
110
+ const body = frac ? `${int}.${frac}` : int;
111
+ return negative && units !== 0n ? `-${body}` : body;
112
+ }
113
+ function assertWholeCount(value, field) {
114
+ if (!Number.isInteger(value) || value < 0) {
115
+ throw new TickAndLotError(`${field} must be a non-negative integer: ${String(value)}`);
116
+ }
117
+ }
118
+ /**
119
+ * L'exposant décimal de la valeur — `2` pour 345, `-3` pour 0,001234 — ou
120
+ * `null` pour zéro, qui n'en a pas.
121
+ */
122
+ function log10Floor(text) {
123
+ const { int, frac } = split(text);
124
+ const significantInt = int.replace(/^0+/, '');
125
+ if (significantInt)
126
+ return significantInt.length - 1;
127
+ const leadingZeros = /^0*/.exec(frac)?.[0].length ?? 0;
128
+ if (leadingZeros === frac.length)
129
+ return null;
130
+ return -(leadingZeros + 1);
131
+ }
132
+ /**
133
+ * Arrondit une valeur à `decimals` décimales, dans le sens demandé, sans
134
+ * jamais passer par un flottant.
135
+ */
136
+ function roundDecimal(value, decimals, mode = 'truncate') {
137
+ assertWholeCount(decimals, 'decimals');
138
+ const { negative, int, frac } = split(toDecimalText(value, 'value'));
139
+ const kept = frac.slice(0, decimals).padEnd(decimals, '0');
140
+ const dropped = frac.slice(decimals);
141
+ let units = BigInt(`${int}${kept}`);
142
+ if (/[1-9]/.test(dropped)) {
143
+ const awayFromZero = mode === 'nearest'
144
+ ? dropped.charAt(0) >= '5'
145
+ : mode === 'up'
146
+ ? !negative
147
+ : mode === 'down'
148
+ ? negative
149
+ : false;
150
+ if (awayFromZero)
151
+ units += 1n;
152
+ }
153
+ return formatUnits(negative, units, decimals);
154
+ }
155
+ /**
156
+ * Le nombre de décimales que ce prix a le droit d'avoir — les deux règles
157
+ * réunies en un seul nombre.
158
+ *
159
+ * Le plancher à zéro **est** l'exception des entiers : quand les chiffres
160
+ * significatifs en réclameraient moins que zéro décimale (un prix au-delà de
161
+ * 99 999), la documentation autorise malgré tout n'importe quel entier, donc
162
+ * on s'arrête à l'entier plutôt que de raboter aussi les unités.
163
+ */
164
+ function priceDecimals(price, szDecimals, type = 'perp') {
165
+ assertWholeCount(szDecimals, 'szDecimals');
166
+ const byDecimals = Math.max(exports.MAX_PRICE_DECIMALS[type] - szDecimals, 0);
167
+ const magnitude = log10Floor(toDecimalText(price, 'price'));
168
+ if (magnitude === null)
169
+ return byDecimals;
170
+ const bySignificantDigits = exports.MAX_PRICE_SIGNIFICANT_DIGITS - 1 - magnitude;
171
+ return Math.min(byDecimals, Math.max(bySignificantDigits, 0));
172
+ }
173
+ /**
174
+ * Le prix, réécrit comme Hyperliquid l'accepte : tronqué à ce que
175
+ * `priceDecimals` autorise.
176
+ *
177
+ * @throws {TickAndLotError} si la valeur est illisible, ou tronquée à zéro.
178
+ */
179
+ function formatPrice(price, szDecimals, type = 'perp') {
180
+ const decimals = priceDecimals(price, szDecimals, type);
181
+ const formatted = roundDecimal(price, decimals, 'truncate');
182
+ if (formatted === '0') {
183
+ throw new TickAndLotError('Price is too small and was truncated to 0');
184
+ }
185
+ return formatted;
186
+ }
187
+ /**
188
+ * La taille, réécrite comme Hyperliquid l'accepte : **tronquée** à
189
+ * `szDecimals`, jamais arrondie. Arrondir vers le haut, ce serait trader plus
190
+ * que ce qui a été calculé — dépasser une position sur un ordre `reduceOnly`,
191
+ * ou le collatéral sur une ouverture.
192
+ *
193
+ * @throws {TickAndLotError} si la valeur est illisible, ou tronquée à zéro.
194
+ */
195
+ function formatSize(size, szDecimals) {
196
+ assertWholeCount(szDecimals, 'szDecimals');
197
+ const formatted = roundDecimal(size, szDecimals, 'truncate');
198
+ if (formatted === '0') {
199
+ throw new TickAndLotError('Size is too small and was truncated to 0');
200
+ }
201
+ return formatted;
202
+ }
203
+ /**
204
+ * Pose un prix sur la grille de l'actif **dans un sens choisi**, et garantit
205
+ * que le résultat en est un point fixe : `formatPrice(snapPrice(p)) === snapPrice(p)`.
206
+ *
207
+ * C'est ce que `formatPrice` seul ne sait pas faire — il tronque toujours.
208
+ * Un émetteur qui veut qu'un stop ne s'éloigne jamais de son ancre a besoin du
209
+ * sens opposé, et il a besoin que le formatage final ne défasse pas ce choix.
210
+ *
211
+ * @throws {TickAndLotError} si la valeur est illisible, ou ramenée à zéro.
212
+ */
213
+ function snapPrice(price, szDecimals, type = 'perp', mode = 'truncate') {
214
+ const snapped = roundDecimal(price, priceDecimals(price, szDecimals, type), mode);
215
+ // Un arrondi vers l'extérieur peut franchir un palier — 9,9999 devient 10 —
216
+ // et la grille change avec l'ordre de grandeur. On la recalcule une fois.
217
+ const settled = roundDecimal(snapped, priceDecimals(snapped, szDecimals, type), mode);
218
+ if (settled === '0') {
219
+ throw new TickAndLotError('Price is too small and was snapped to 0');
220
+ }
221
+ if (formatPrice(settled, szDecimals, type) !== settled) {
222
+ throw new TickAndLotError(`snapPrice produced ${settled}, which is not on the grid for szDecimals ${szDecimals}`);
223
+ }
224
+ return settled;
225
+ }
package/dist/index.d.ts CHANGED
@@ -1 +1,3 @@
1
1
  export * from './interfaces';
2
+ export * from './format';
3
+ export * from './market';
package/dist/index.js CHANGED
@@ -15,3 +15,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./interfaces"), exports);
18
+ __exportStar(require("./format"), exports);
19
+ __exportStar(require("./market"), exports);
@@ -0,0 +1 @@
1
+ export * from './market-name';
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./market-name"), exports);
@@ -0,0 +1,54 @@
1
+ /**
2
+ * ============================================================================
3
+ * LE NOM D'UN MARCHÉ DIT DE QUEL CATALOGUE IL RELÈVE
4
+ *
5
+ * Source : doc Hyperliquid « Asset IDs », relevée le 2026-09-30.
6
+ *
7
+ * - un perp déployé par un builder porte **toujours** un nom `{dex}:{coin}` —
8
+ * la doc écrit « always », ce n'est pas une heuristique ;
9
+ * - une paire spot se désigne par `10000 + spotInfo.index`, d'où la forme
10
+ * protocolaire `@<index>` ; seules les canoniques portent `BASE/QUOTE`.
11
+ * L'exemple HYPE de la doc (token 150, spot 107) recoupe le relevé du
12
+ * 2026-09-30 : le solde porte `token: 150`, la paire s'appelle `@107` ;
13
+ * - tout le reste est un perp du dex principal, que l'API désigne par un `dex`
14
+ * vide.
15
+ *
16
+ * Ce module vit ici, et non dans le bot ou le mobile, pour la même raison que
17
+ * `tick-and-lot` : qu'il n'en existe **qu'une seule implémentation**. Les deux
18
+ * doivent décider à l'identique de quel univers relève un marché — sans quoi
19
+ * l'un signalerait comme morte une paire que l'autre continuerait de trader,
20
+ * ou l'inverse. Une divergence silencieuse entre l'écran et le moteur est
21
+ * exactement ce qu'on ne peut pas se permettre ici.
22
+ *
23
+ * L'incident qui l'a motivé, le 2026-09-30 : `vntl:ROBOT` était configurée sur
24
+ * l'écran Bot Strategies, ratio et protections comprises, sur un dex dont les
25
+ * 15 marchés sont délistés — et rien ne le disait. Le dex principal n'est pas
26
+ * épargné : 56 de ses 234 marchés le sont aussi.
27
+ *
28
+ * ⚠️ Le piège est le spot. Sur 330 paires spot relevées, la grande majorité
29
+ * s'appelle `@N` et ne porte **ni** barre oblique **ni** deux-points. Les
30
+ * traiter par défaut comme des perps les ferait chercher — en vain — dans
31
+ * l'univers du dex principal, et déclarer inexistant un marché vivant.
32
+ * ============================================================================
33
+ */
34
+ /** Ce qu'un nom de marché désigne, du point de vue du catalogue à consulter. */
35
+ export type HLMarketKind =
36
+ /** Perp du dex principal : `BTC`, `ETH`. L'API le désigne par un `dex` vide. */
37
+ 'perp'
38
+ /** Perp déployé par un builder (HIP-3) : `vntl:ROBOT`, `xyz:AAPL`. */
39
+ | 'builderPerp'
40
+ /** Paire spot : `PURR/USDC` (canonique) ou `@107` (forme protocolaire). */
41
+ | 'spot';
42
+ /**
43
+ * Le genre de marché que ce nom désigne, ou `null` quand le nom n'en désigne
44
+ * aucun.
45
+ */
46
+ export declare function hlMarketKind(name: string): HLMarketKind | null;
47
+ /**
48
+ * Le dex perp dont relève ce marché — `''` pour le dex principal, le préfixe
49
+ * pour un HIP-3 —, ou `null` quand ce n'est pas un marché perp.
50
+ *
51
+ * C'est la valeur à passer à `meta({ dex })` : l'API accepte la chaîne vide
52
+ * pour le dex principal, et `null` dit qu'il n'y a rien à charger.
53
+ */
54
+ export declare function hlPerpDexOf(name: string): string | null;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ /**
3
+ * ============================================================================
4
+ * LE NOM D'UN MARCHÉ DIT DE QUEL CATALOGUE IL RELÈVE
5
+ *
6
+ * Source : doc Hyperliquid « Asset IDs », relevée le 2026-09-30.
7
+ *
8
+ * - un perp déployé par un builder porte **toujours** un nom `{dex}:{coin}` —
9
+ * la doc écrit « always », ce n'est pas une heuristique ;
10
+ * - une paire spot se désigne par `10000 + spotInfo.index`, d'où la forme
11
+ * protocolaire `@<index>` ; seules les canoniques portent `BASE/QUOTE`.
12
+ * L'exemple HYPE de la doc (token 150, spot 107) recoupe le relevé du
13
+ * 2026-09-30 : le solde porte `token: 150`, la paire s'appelle `@107` ;
14
+ * - tout le reste est un perp du dex principal, que l'API désigne par un `dex`
15
+ * vide.
16
+ *
17
+ * Ce module vit ici, et non dans le bot ou le mobile, pour la même raison que
18
+ * `tick-and-lot` : qu'il n'en existe **qu'une seule implémentation**. Les deux
19
+ * doivent décider à l'identique de quel univers relève un marché — sans quoi
20
+ * l'un signalerait comme morte une paire que l'autre continuerait de trader,
21
+ * ou l'inverse. Une divergence silencieuse entre l'écran et le moteur est
22
+ * exactement ce qu'on ne peut pas se permettre ici.
23
+ *
24
+ * L'incident qui l'a motivé, le 2026-09-30 : `vntl:ROBOT` était configurée sur
25
+ * l'écran Bot Strategies, ratio et protections comprises, sur un dex dont les
26
+ * 15 marchés sont délistés — et rien ne le disait. Le dex principal n'est pas
27
+ * épargné : 56 de ses 234 marchés le sont aussi.
28
+ *
29
+ * ⚠️ Le piège est le spot. Sur 330 paires spot relevées, la grande majorité
30
+ * s'appelle `@N` et ne porte **ni** barre oblique **ni** deux-points. Les
31
+ * traiter par défaut comme des perps les ferait chercher — en vain — dans
32
+ * l'univers du dex principal, et déclarer inexistant un marché vivant.
33
+ * ============================================================================
34
+ */
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.hlMarketKind = hlMarketKind;
37
+ exports.hlPerpDexOf = hlPerpDexOf;
38
+ /**
39
+ * Le genre de marché que ce nom désigne, ou `null` quand le nom n'en désigne
40
+ * aucun.
41
+ */
42
+ function hlMarketKind(name) {
43
+ if (!name)
44
+ return null;
45
+ // Le spot se teste en premier : un nom peut porter les deux marques
46
+ // (`cash:XYZ/USDT`), et c'est alors la paire qui prime sur le dex.
47
+ if (name.includes('/') || name.startsWith('@'))
48
+ return 'spot';
49
+ const separator = name.indexOf(':');
50
+ if (separator === -1)
51
+ return 'perp';
52
+ // Un nom de builder doit porter ses deux moitiés. Les accepter amputées
53
+ // serait pire que les refuser : `:BTC` rendrait un dex vide, donc le dex
54
+ // **principal**, et ferait juger un marché contre un univers qui n'est pas
55
+ // le sien.
56
+ const dex = name.slice(0, separator);
57
+ const coin = name.slice(separator + 1);
58
+ return dex && coin ? 'builderPerp' : null;
59
+ }
60
+ /**
61
+ * Le dex perp dont relève ce marché — `''` pour le dex principal, le préfixe
62
+ * pour un HIP-3 —, ou `null` quand ce n'est pas un marché perp.
63
+ *
64
+ * C'est la valeur à passer à `meta({ dex })` : l'API accepte la chaîne vide
65
+ * pour le dex principal, et `null` dit qu'il n'y a rien à charger.
66
+ */
67
+ function hlPerpDexOf(name) {
68
+ const kind = hlMarketKind(name);
69
+ if (kind === 'perp')
70
+ return '';
71
+ if (kind === 'builderPerp')
72
+ return name.slice(0, name.indexOf(':'));
73
+ return null;
74
+ }
package/package.json CHANGED
@@ -1,50 +1,54 @@
1
- {
2
- "name": "@syldel/hl-shared-types",
3
- "version": "0.0.16",
4
- "description": "Shared TypeScript interfaces and types for Hyperliquid integration.",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/Syldel/hl-shared-types.git"
10
- },
11
- "bugs": {
12
- "url": "https://github.com/Syldel/hl-shared-types/issues"
13
- },
14
- "homepage": "https://github.com/Syldel/hl-shared-types#readme",
15
- "files": [
16
- "dist"
17
- ],
18
- "scripts": {
19
- "lint": "eslint .",
20
- "lint:fix": "eslint . --fix",
21
- "clean": "rm -rf dist",
22
- "build": "npm run lint && npm run clean && tsc",
23
- "prepublishOnly": "npm run build",
24
- "release": "npm run build && npm version patch && npm publish"
25
- },
26
- "publishConfig": {
27
- "access": "public"
28
- },
29
- "keywords": [
30
- "hyperliquid",
31
- "trading",
32
- "types",
33
- "interfaces",
34
- "crypto",
35
- "exchange",
36
- "defi"
37
- ],
38
- "author": "Syl. D.",
39
- "license": "ISC",
40
- "devDependencies": {
41
- "eslint": "^9.39.2",
42
- "eslint-config-prettier": "^10.1.8",
43
- "eslint-plugin-jsonc": "^2.21.0",
44
- "eslint-plugin-prettier": "^5.5.5",
45
- "jsonc-eslint-parser": "^2.4.2",
46
- "prettier": "^3.8.0",
47
- "typescript": "^5.3.3",
48
- "typescript-eslint": "^8.53.0"
49
- }
50
- }
1
+ {
2
+ "name": "@syldel/hl-shared-types",
3
+ "version": "0.0.18",
4
+ "description": "Shared TypeScript interfaces and types for Hyperliquid integration.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Syldel/hl-shared-types.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/Syldel/hl-shared-types/issues"
13
+ },
14
+ "homepage": "https://github.com/Syldel/hl-shared-types#readme",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "test": "jest",
20
+ "lint": "eslint .",
21
+ "lint:fix": "eslint . --fix",
22
+ "clean": "rm -rf dist",
23
+ "build": "npm run lint && npm run test && npm run clean && tsc",
24
+ "prepublishOnly": "npm run build",
25
+ "release": "npm run build && npm version patch && npm publish"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "keywords": [
31
+ "hyperliquid",
32
+ "trading",
33
+ "types",
34
+ "interfaces",
35
+ "crypto",
36
+ "exchange",
37
+ "defi"
38
+ ],
39
+ "author": "Syl. D.",
40
+ "license": "ISC",
41
+ "devDependencies": {
42
+ "@types/jest": "^30.0.0",
43
+ "eslint": "^9.39.2",
44
+ "eslint-config-prettier": "^10.1.8",
45
+ "eslint-plugin-jsonc": "^2.21.0",
46
+ "eslint-plugin-prettier": "^5.5.5",
47
+ "jest": "^30.5.2",
48
+ "jsonc-eslint-parser": "^2.4.2",
49
+ "prettier": "^3.8.0",
50
+ "ts-jest": "^29.4.13",
51
+ "typescript": "^5.3.3",
52
+ "typescript-eslint": "^8.53.0"
53
+ }
54
+ }