@blackcube/xgate-sdk 0.20.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,7 +58,40 @@ Chacun est un `@Injectable()` NestJS, utilisable seul ou via `XgateModule`.
58
58
  | `WsCandlesService` | les bougies en temps réel, un handler pour N venues |
59
59
  | `PricesService` | mark, oracle, mid, bid/ask, funding, open interest |
60
60
  | `WalletService` | soldes, mouvements, historique d'équité |
61
- | `TradingService` | positions, ordres, exécutions, ouverture protégée, clôture |
61
+ | `TradingService` | positions, ordres, exécutions — et le cycle **ouvrir → gérer → fermer** |
62
+
63
+ Le cycle de vie d'une position tient en trois gestes, et chacun préserve le même invariant : la
64
+ position n'est jamais sans stop.
65
+
66
+ | geste | méthode | venues |
67
+ |---|---|---|
68
+ | ouvrir avec sa protection | `openWithProtection` | hyperliquid, pacifica |
69
+ | déplacer le stop | `moveStop` | hyperliquid, pacifica, aster |
70
+ | fermer sans reliquat | `closePosition` | toutes celles qui tradent |
71
+ | relire le prix de sortie réel | `exitPrice` | toutes celles qui tradent |
72
+
73
+ ## Le portefeuille
74
+
75
+ `balances()` rend **le comptant et le collatéral**, chaque ligne portant sa nature :
76
+
77
+ ```typescript
78
+ const soldes = await wallet.balances(access);
79
+ soldes.filter((solde) => solde.xtras?.scope === 'perp'); // la marge
80
+ soldes.filter((solde) => solde.xtras?.scope !== 'perp'); // le comptant
81
+ ```
82
+
83
+ Les SDK de venue, eux, ne rendent que le comptant — c'est leur convention, et la documentation de
84
+ chacune le confirme. **Sans le collatéral, un portefeuille peut paraître vide** : sur un compte
85
+ pacifica dont tout le solde est en marge, les soldes comptant valent `[]`, ce qui se lit « pas
86
+ d'argent » alors qu'il y a 499 USDC. XGate lit donc aussi la marge et l'ajoute, marquée.
87
+
88
+ ```
89
+ pacifica USDC 499.204355 (perp)
90
+ hyperliquid USDC 1137.565924 (spot) + USDC 7.320954 (perp)
91
+ ```
92
+
93
+ Les deux lignes s'additionnent sans se recouvrir : le collatéral est hors comptant chez les deux
94
+ venues.
62
95
 
63
96
  ## Le trading
64
97
 
@@ -96,6 +129,38 @@ const orders = await trading.openWithProtection(access, {
96
129
 
97
130
  Le premier ordre rendu est l'entrée ; les suivants sont le stop et les take-profits.
98
131
 
132
+ ### Déplacer le stop
133
+
134
+ Le geste du point mort : une position qui a couru se sécurise en remontant son stop. Le SDK de la
135
+ venue pose le **nouveau** stop avant d'annuler l'ancien — la position n'est jamais nue, pas même une
136
+ milliseconde.
137
+
138
+ ```typescript
139
+ const carnet = await trading.openOrders(access);
140
+ const stop = carnet.find((order) => order.symbolXex === 'ETH' && order.reduceOnly === true);
141
+
142
+ await trading.moveStop(access, {
143
+ xex: XgateEx.Hyperliquid,
144
+ symbolXex: 'ETH',
145
+ direction: 'long',
146
+ stopId: stop.id,
147
+ triggerPrice: 3120, // arrondi au tick par XGate
148
+ size: 0.02,
149
+ }, markPrice);
150
+ ```
151
+
152
+ **Disponible sur hyperliquid, pacifica et aster** — les trois seules venues qui exposent la
153
+ capacité. Ailleurs la méthode lève : déplacer un stop y demanderait d'annuler puis de reposer, ce
154
+ qui rouvre exactement la fenêtre nue que tout ceci existe pour fermer.
155
+
156
+ **Un stop du mauvais côté est refusé avant l'appel réseau.** Pour un long, un stop au-dessus du
157
+ marché se déclenche immédiatement et solde la position — la venue l'accepte sans broncher, et
158
+ l'erreur ne se voit qu'une fois l'argent parti. Le prix courant est donc un argument obligatoire.
159
+
160
+ > **L'identifiant du stop ne vient pas de l'ouverture.** Hyperliquid répond `"waitingForTrigger"` —
161
+ > une chaîne nue, sans identifiant — pour un stop groupé sous son entrée. Il se retrouve au carnet,
162
+ > où `reduceOnly === true` le distingue.
163
+
99
164
  ### Fermer
100
165
 
101
166
  `closePosition` relit la position **sur la venue** avant de la fermer, et sort la taille que la venue
@@ -106,6 +171,19 @@ fill a différé du plan, et ce reliquat reste ouvert, sans stop.
106
171
  await trading.closePosition(access, 'ETH'); // null s'il n'y avait rien à fermer
107
172
  ```
108
173
 
174
+ ### Le prix de sortie réel
175
+
176
+ Le prix planifié ne dit pas à quel prix on est sorti : un stop se déclenche au marché, un take-profit
177
+ peut remplir en plusieurs fois. `exitPrice` relit l'historique de la venue et rend le prix du
178
+ **dernier ordre de sortie réellement rempli** — c'est lui qui doit servir au calcul du résultat,
179
+ sinon le PnL affiché est une fiction.
180
+
181
+ ```typescript
182
+ const prix = await trading.exitPrice(access, 'ETH', 'long', ouvertureDate);
183
+ ```
184
+
185
+ Rend `null` quand la venue n'expose rien d'exploitable ; à l'appelant de décider de son repli.
186
+
109
187
  ### Le réseau est explicite
110
188
 
111
189
  `network` vaut `mainnet` par défaut. **Le préciser est la seule chose qui sépare un test d'un ordre
@@ -137,6 +215,15 @@ passerelle tel quel : `ORDER_STATUSES` est exporté pour valider plutôt que cas
137
215
  - **Une taille de position est toujours positive** : le sens est porté par `side`.
138
216
  - **Pas de mensuel.** Les intervalles s'arrêtent à `1w`.
139
217
 
218
+ ## Versions
219
+
220
+ La famille avance **en version iso** : XGate et les SDK qu'il agrège portent le même numéro, pour
221
+ qu'un `0.20.0` désigne le même état partout et qu'aucune combinaison ne soit à vérifier.
222
+
223
+ **bullet est l'exception, et elle est délibérée** : il suit **son propre cycle** (`0.55.x`), parce
224
+ qu'il était déjà publié bien plus haut quand la famille s'est alignée. Le forcer à redescendre
225
+ inventerait une version antérieure à ce qui existe. XGate le référence donc à son numéro à lui.
226
+
140
227
  ## Documentation
141
228
 
142
229
  Le détail par service vit dans les docblocks — ils portent le *pourquoi*, pas la paraphrase du code.
package/dist/index.cjs CHANGED
@@ -761,6 +761,7 @@ var ORDER_STATUSES = [
761
761
 
762
762
  // src/services/trading.service.ts
763
763
  var VENUES_PROUVEES = ["pacifica" /* Pacifica */, "hyperliquid" /* Hyperliquid */];
764
+ var VENUES_MOVE_STOP = ["hyperliquid" /* Hyperliquid */, "pacifica" /* Pacifica */, "aster" /* Aster */];
764
765
  exports.TradingService = class TradingService {
765
766
  logger = new common.Logger(exports.TradingService.name);
766
767
  /**
@@ -901,6 +902,45 @@ exports.TradingService = class TradingService {
901
902
  }
902
903
  return closed;
903
904
  }
905
+ /**
906
+ * DÉPLACE LE STOP d'une position ouverte — le geste du point mort.
907
+ *
908
+ * Le SDK de la venue pose le NOUVEAU stop **avant** d'annuler l'ancien : la position n'est jamais
909
+ * nue, pas même une milliseconde. C'est le même invariant qu'à l'ouverture, appliqué au milieu de
910
+ * la vie de la position.
911
+ *
912
+ * **Un stop du mauvais côté est refusé ici**, avant l'appel réseau. Pour un long, un stop au-dessus
913
+ * du marché se déclenche immédiatement et solde la position — la venue l'accepte sans broncher, et
914
+ * l'erreur ne se voit qu'une fois l'argent parti. Ce refus n'est pas de la prudence décorative :
915
+ * il a été écrit après avoir vu le cas se produire.
916
+ */
917
+ async moveStop(access, input, markPrice) {
918
+ const trigger = roundPrice(input.triggerPrice, input.tickSize, input.lotSize);
919
+ const perdant = input.direction === "long" ? trigger >= markPrice : trigger <= markPrice;
920
+ if (perdant === true) {
921
+ throw new Error(
922
+ `moveStop(${access.xex}/${input.symbolXex}) : un stop ${input.direction} \xE0 ${trigger} est du mauvais c\xF4t\xE9 du march\xE9 (${markPrice}) \u2014 il se d\xE9clencherait aussit\xF4t et solderait la position.`
923
+ );
924
+ }
925
+ const perp = this.perpOf(access);
926
+ if (typeof perp.moveStop !== "function") {
927
+ throw new Error(
928
+ `moveStop(${access.xex}) : cette venue ne sait pas d\xE9placer un stop. Disponible sur ${VENUES_MOVE_STOP.join(", ")}.`
929
+ );
930
+ }
931
+ const glissement = input.slippagePct ?? 5e-3;
932
+ const borne = input.direction === "long" ? 1 - glissement : 1 + glissement;
933
+ const moved = await perp.moveStop({
934
+ name: input.symbolXex,
935
+ side: input.direction === "long" ? "buy" : "sell",
936
+ stopId: input.stopId,
937
+ triggerPrice: String(trigger),
938
+ size: String(roundToStep(input.size, input.lotSize, "floor")),
939
+ price: String(roundPrice(trigger * borne, input.tickSize, input.lotSize))
940
+ });
941
+ this.logger.log(`moveStop(${access.xex}/${input.symbolXex}) : stop port\xE9 \xE0 ${trigger}`);
942
+ return { symbolXex: moved.name, id: moved.id };
943
+ }
904
944
  /**
905
945
  * LE PRIX DE SORTIE RÉEL d'une position, lu dans l'historique de la venue.
906
946
  *
@@ -973,8 +1013,9 @@ exports.WalletService = class WalletService {
973
1013
  */
974
1014
  async balances(...accesses) {
975
1015
  return this.collect(accesses, "balances", async (access) => {
976
- const balances = await createAccountXex(access.xex, access).account().getBalances();
977
- return balances.map((balance) => ({
1016
+ const source = createAccountXex(access.xex, access);
1017
+ const balances = await source.account().getBalances();
1018
+ const lignes = balances.map((balance) => ({
978
1019
  xex: access.xex,
979
1020
  asset: balance.asset,
980
1021
  total: balance.total,
@@ -982,8 +1023,48 @@ exports.WalletService = class WalletService {
982
1023
  usdValue: balance.usdValue,
983
1024
  xtras: balance.xtras
984
1025
  }));
1026
+ const collateral = await this.perpCollateral(source, access.xex);
1027
+ return collateral === null ? lignes : [...lignes, collateral];
985
1028
  });
986
1029
  }
1030
+ /**
1031
+ * LE COLLATÉRAL PERPÉTUEL, ajouté aux soldes comptant — sans lui, un portefeuille peut paraître VIDE.
1032
+ *
1033
+ * `getBalances` ne rend que le **comptant**, chez les huit venues : c'est leur convention, et la
1034
+ * documentation de chacune le dit. Or l'argent qui sert à trader n'y est pas. Sur un compte
1035
+ * pacifica dont tout le solde est en marge, `getBalances` rend `[]` — ce qui se lit « pas
1036
+ * d'argent » alors qu'il y a 499 USDC. Sur hyperliquid, le comptant s'affiche mais la marge
1037
+ * manque.
1038
+ *
1039
+ * La ligne est **marquée `scope: 'perp'`** dans `xtras` : elle s'additionne au comptant sans le
1040
+ * doubler, et un appelant qui ne veut que l'un des deux peut trancher.
1041
+ *
1042
+ * Le champ diffère par venue et n'est pas devinable — d'où la lecture explicite, venue par venue,
1043
+ * plutôt qu'un parcours à l'aveugle d'un objet `unknown`.
1044
+ */
1045
+ async perpCollateral(source, xex) {
1046
+ const facade = source;
1047
+ if (typeof facade.perp !== "function") {
1048
+ return null;
1049
+ }
1050
+ const scope = facade.perp();
1051
+ if (typeof scope.getAccountInfo !== "function") {
1052
+ return null;
1053
+ }
1054
+ const info = await scope.getAccountInfo();
1055
+ const total = info.balance ?? info.marginSummary?.accountValue;
1056
+ if (total === void 0 || Number(total) === 0) {
1057
+ return null;
1058
+ }
1059
+ return {
1060
+ xex,
1061
+ asset: "USDC",
1062
+ total,
1063
+ available: info.availableToSpend ?? info.withdrawable ?? null,
1064
+ usdValue: total,
1065
+ xtras: { scope: "perp" }
1066
+ };
1067
+ }
987
1068
  /**
988
1069
  * Les mouvements d'argent, triés du plus ancien au plus récent, montants **signés**.
989
1070
  *