@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 +88 -1
- package/dist/index.cjs +83 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -1
- package/dist/index.d.ts +61 -1
- package/dist/index.js +83 -2
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
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
|
|
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
|
|
977
|
-
|
|
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
|
*
|