@blackcube/xgate-sdk 0.25.5 → 0.56.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 +10 -0
- package/dist/index.cjs +166 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +84 -2
- package/dist/index.d.ts +84 -2
- package/dist/index.js +166 -5
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -226,10 +226,20 @@ donc à risquer plusieurs sockets pour la même venue.
|
|
|
226
226
|
Le cycle de vie d'une position tient en trois gestes, et chacun préserve le même invariant : la
|
|
227
227
|
position n'est jamais sans stop.
|
|
228
228
|
|
|
229
|
+
**Tout ce qui part est à la grille de la paire** — l'entrée comme le stop et les cibles (`tickSize`,
|
|
230
|
+
sinon la règle hyperliquid : cinq chiffres significatifs, `6 − szDecimals` décimales). Jusqu'en
|
|
231
|
+
0.56.0 l'entrée partait telle quelle ; pacifica refuse un prix hors tick.
|
|
232
|
+
|
|
233
|
+
**Pacifica n'embarque qu'un take-profit dans l'ordre d'entrée.** Les suivants sont posés par XGate en
|
|
234
|
+
déclenchés reduce-only après une entrée immédiate (`ioc`, `fok`), une fois la position relue ; après
|
|
235
|
+
une entrée qui peut rester au carnet (`gtc`, `alo`), c'est l'appelant qui les pose par
|
|
236
|
+
`completeProtection` quand il constate le fill — une fois, jamais deux.
|
|
237
|
+
|
|
229
238
|
| geste | méthode | venues |
|
|
230
239
|
|---|---|---|
|
|
231
240
|
| régler le levier | `setLeverage` | toutes sauf bullet |
|
|
232
241
|
| ouvrir avec sa protection | `openWithProtection` | hyperliquid, pacifica |
|
|
242
|
+
| poser les cibles qu'une venue n'embarque pas, une fois l'entrée remplie | `completeProtection` | pacifica (rien à poser ailleurs) |
|
|
233
243
|
| déplacer le stop | `moveStop` | hyperliquid, pacifica, aster |
|
|
234
244
|
| fermer sans reliquat | `closePosition` | toutes celles qui tradent |
|
|
235
245
|
| relire le prix de sortie réel | `exitPrice` | toutes celles qui tradent |
|
package/dist/index.cjs
CHANGED
|
@@ -679,6 +679,57 @@ exports.CandlesService = __decorateClass([
|
|
|
679
679
|
common.Injectable()
|
|
680
680
|
], exports.CandlesService);
|
|
681
681
|
|
|
682
|
+
// src/helpers/asset-class.ts
|
|
683
|
+
var BINANCE = {
|
|
684
|
+
COIN: "crypto",
|
|
685
|
+
EQUITY: "stock",
|
|
686
|
+
CN_EQUITY: "stock",
|
|
687
|
+
HK_EQUITY: "stock",
|
|
688
|
+
KR_EQUITY: "stock",
|
|
689
|
+
PREMARKET: "stock",
|
|
690
|
+
COMMODITY: "commodity",
|
|
691
|
+
INDEX: "index"
|
|
692
|
+
};
|
|
693
|
+
var BYBIT = {
|
|
694
|
+
"": "crypto",
|
|
695
|
+
innovation: "crypto",
|
|
696
|
+
stock: "stock",
|
|
697
|
+
commodity: "commodity",
|
|
698
|
+
ETF: "etf"
|
|
699
|
+
};
|
|
700
|
+
var EXTENDED = {
|
|
701
|
+
Equity: "stock",
|
|
702
|
+
Commodity: "commodity",
|
|
703
|
+
"ETF/Index": "index",
|
|
704
|
+
FX: "forex",
|
|
705
|
+
"Pre-market": "stock"
|
|
706
|
+
};
|
|
707
|
+
function assetClassOf(xex, xtras) {
|
|
708
|
+
if (xtras === void 0) {
|
|
709
|
+
return void 0;
|
|
710
|
+
}
|
|
711
|
+
if (xex === "binance") {
|
|
712
|
+
const brut = xtras.underlyingType;
|
|
713
|
+
return typeof brut === "string" ? BINANCE[brut] : void 0;
|
|
714
|
+
}
|
|
715
|
+
if (xex === "bybit") {
|
|
716
|
+
const brut = xtras.symbolType;
|
|
717
|
+
return typeof brut === "string" ? BYBIT[brut] : void 0;
|
|
718
|
+
}
|
|
719
|
+
if (xex === "extended") {
|
|
720
|
+
const categorie = xtras.category;
|
|
721
|
+
if (typeof categorie !== "string") {
|
|
722
|
+
return void 0;
|
|
723
|
+
}
|
|
724
|
+
if (categorie !== "RWA") {
|
|
725
|
+
return "crypto";
|
|
726
|
+
}
|
|
727
|
+
const sous = xtras.subCategory;
|
|
728
|
+
return typeof sous === "string" ? EXTENDED[sous] : void 0;
|
|
729
|
+
}
|
|
730
|
+
return void 0;
|
|
731
|
+
}
|
|
732
|
+
|
|
682
733
|
// src/helpers/contract-expiry.ts
|
|
683
734
|
var DATED_CONTRACT_TYPES = /* @__PURE__ */ new Set([
|
|
684
735
|
// binance : les trimestriels (`BTCUSDT_260925`, `ETHUSDT_261225`).
|
|
@@ -761,6 +812,7 @@ exports.CatalogService = class CatalogService {
|
|
|
761
812
|
xex,
|
|
762
813
|
kind: pair.kind,
|
|
763
814
|
multiplier: multiplierFromXex(pair.name),
|
|
815
|
+
assetClass: assetClassOf(xex, pair.xtras),
|
|
764
816
|
expiresAt: expiryOf(pair.xtras),
|
|
765
817
|
ref: pair.ref ?? null,
|
|
766
818
|
szDecimals: pair.szDecimals,
|
|
@@ -949,6 +1001,7 @@ exports.SpotCatalogService = class SpotCatalogService {
|
|
|
949
1001
|
// Le comptant porte les mêmes échelles que le perp : `SATSUSDT` (×1) et `1000SATSUSDT`
|
|
950
1002
|
// coexistent, et c'est justement entre eux que la comparaison serait fausse.
|
|
951
1003
|
multiplier: multiplierFromXex(pair.name),
|
|
1004
|
+
assetClass: assetClassOf(xex, pair.xtras),
|
|
952
1005
|
ref: pair.ref ?? null,
|
|
953
1006
|
szDecimals: pair.szDecimals,
|
|
954
1007
|
tickSize: pair.tickSize,
|
|
@@ -1205,6 +1258,21 @@ exports.TradingService = class TradingService {
|
|
|
1205
1258
|
* Chaque venue applique son mécanisme natif — hyperliquid groupe les enfants sous l'entrée et les
|
|
1206
1259
|
* annule lui-même si elle rate, pacifica les embarque dans l'ordre, aster envoie un lot de
|
|
1207
1260
|
* conditionnels. Le SDK de la venue s'en charge ; xgate fournit des niveaux justes.
|
|
1261
|
+
*
|
|
1262
|
+
* **LE PRIX D'ENTRÉE EST ARRONDI À LA GRILLE DE LA PAIRE, comme les protections.** Jusqu'en 0.56.0 il
|
|
1263
|
+
* partait tel quel (`String(input.entry)`) : les protections étaient justes, l'entrée non — et
|
|
1264
|
+
* pacifica l'envoie au wire sans le reformater (hyperliquid, lui, le reformate). Mesuré le
|
|
1265
|
+
* 2026-09-12 en rejouant 171 ouvertures : 79 entrées hors grille (`13.7421` sur un marché à cinq
|
|
1266
|
+
* chiffres significatifs, `75.885` sur un tick de 0,01). Les niveaux en pourcentage se calculent
|
|
1267
|
+
* depuis l'entrée ARRONDIE — celle qui sera réellement posée.
|
|
1268
|
+
*
|
|
1269
|
+
* **LES CIBLES QUE LA VENUE N'EMBARQUE PAS SONT POSÉES ICI.** Pacifica n'embarque qu'un take-profit
|
|
1270
|
+
* dans l'ordre d'entrée et rend les suivants par `pendingTps()` — « à appeler une fois le fill
|
|
1271
|
+
* constaté ». Avec une entrée immédiate (`ioc`, `fok`), le fill est constaté tout de suite : on
|
|
1272
|
+
* relit la position et on pose ce qui manque, en ordres déclenchés reduce-only. Avec une entrée
|
|
1273
|
+
* qui peut rester au carnet (`gtc`, `alo`), rien ne peut être posé avant le fill : l'appelant
|
|
1274
|
+
* complète par {@link completeProtection} quand il le constate. Jusqu'en 0.56.0 ces cibles n'étaient
|
|
1275
|
+
* posées par personne : une position tide à trois paliers sur pacifica partait sans sa dernière.
|
|
1208
1276
|
*/
|
|
1209
1277
|
async openWithProtection(access, input) {
|
|
1210
1278
|
if (VENUES_PROUVEES.includes(access.xex) === false) {
|
|
@@ -1212,8 +1280,9 @@ exports.TradingService = class TradingService {
|
|
|
1212
1280
|
`openWithProtection(${access.xex}) : l'ouverture prot\xE9g\xE9e n'a pas \xE9t\xE9 prouv\xE9e en r\xE9el sur cette venue. Seules ${VENUES_PROUVEES.join(", ")} le sont. Ouvrir ici reviendrait \xE0 parier qu'un chemin jamais exerc\xE9 pose bien le stop.`
|
|
1213
1281
|
);
|
|
1214
1282
|
}
|
|
1283
|
+
const entryPrice = roundPrice(input.entry, input.tickSize, input.lotSize);
|
|
1215
1284
|
const protection = buildProtection({
|
|
1216
|
-
entry:
|
|
1285
|
+
entry: entryPrice,
|
|
1217
1286
|
direction: input.direction,
|
|
1218
1287
|
size: input.size,
|
|
1219
1288
|
sl: input.sl,
|
|
@@ -1229,13 +1298,14 @@ exports.TradingService = class TradingService {
|
|
|
1229
1298
|
`openWithProtection(${access.xex}) : cette venue n'expose pas l'ouverture prot\xE9g\xE9e atomique.`
|
|
1230
1299
|
);
|
|
1231
1300
|
}
|
|
1301
|
+
const tif = input.tif ?? "ioc";
|
|
1232
1302
|
const entry = {
|
|
1233
1303
|
name: input.symbolXex,
|
|
1234
1304
|
side,
|
|
1235
1305
|
type: "limit",
|
|
1236
1306
|
size: String(input.size),
|
|
1237
|
-
price: String(
|
|
1238
|
-
tif
|
|
1307
|
+
price: String(entryPrice),
|
|
1308
|
+
tif,
|
|
1239
1309
|
reduceOnly: false,
|
|
1240
1310
|
clientId: input.clientId
|
|
1241
1311
|
};
|
|
@@ -1248,7 +1318,25 @@ exports.TradingService = class TradingService {
|
|
|
1248
1318
|
};
|
|
1249
1319
|
try {
|
|
1250
1320
|
const orders = await perp.createEntryWithProtection(entry, consigne);
|
|
1251
|
-
|
|
1321
|
+
const posees = orders.map((order) => this.toOrder(order, access.xex));
|
|
1322
|
+
const enAttente = this.ciblesEnAttente(perp, consigne);
|
|
1323
|
+
if (enAttente.length === 0) {
|
|
1324
|
+
return posees;
|
|
1325
|
+
}
|
|
1326
|
+
if (tif !== "ioc" && tif !== "fok") {
|
|
1327
|
+
this.logger.warn(
|
|
1328
|
+
`openWithProtection(${access.xex}/${input.symbolXex}) : ${enAttente.length} cible(s) que la venue n'embarque pas \u2014 l'entr\xE9e ${tif} peut rester au carnet, appeler completeProtection une fois remplie.`
|
|
1329
|
+
);
|
|
1330
|
+
return posees;
|
|
1331
|
+
}
|
|
1332
|
+
const complement = await this.poserCiblesEnAttente(
|
|
1333
|
+
access,
|
|
1334
|
+
perp,
|
|
1335
|
+
input.symbolXex,
|
|
1336
|
+
side,
|
|
1337
|
+
enAttente
|
|
1338
|
+
);
|
|
1339
|
+
return [...posees, ...complement];
|
|
1252
1340
|
} catch (error) {
|
|
1253
1341
|
const message = error instanceof Error ? error.message : String(error);
|
|
1254
1342
|
if (message.includes(ASTER_TIMEOUT) === false) {
|
|
@@ -1260,6 +1348,79 @@ exports.TradingService = class TradingService {
|
|
|
1260
1348
|
return await this.etatApres(access, input.symbolXex);
|
|
1261
1349
|
}
|
|
1262
1350
|
}
|
|
1351
|
+
/**
|
|
1352
|
+
* POSE LES CIBLES QUE LA VENUE N'A PAS EMBARQUÉES, une fois l'entrée remplie.
|
|
1353
|
+
*
|
|
1354
|
+
* Le pendant de {@link openWithProtection} pour une entrée qui pouvait rester au carnet (`gtc`,
|
|
1355
|
+
* `alo`) : l'appelant constate le fill — une position sur la paire — et complète. Les niveaux se
|
|
1356
|
+
* recalculent depuis la même consigne, donc les mêmes prix ; la venue qui embarque toutes ses
|
|
1357
|
+
* cibles (hyperliquid, aster) n'a rien en attente et rien n'est posé. Sans position, rien non plus :
|
|
1358
|
+
* un ordre reduce-only sans position serait refusé ou annulé par la venue.
|
|
1359
|
+
*
|
|
1360
|
+
* ⚠️ NON IDEMPOTENT : appelé deux fois après le même fill, il pose les cibles deux fois. L'appelant
|
|
1361
|
+
* le fait UNE fois, quand sa relecture montre la position.
|
|
1362
|
+
*/
|
|
1363
|
+
async completeProtection(access, input) {
|
|
1364
|
+
const entryPrice = roundPrice(input.entry, input.tickSize, input.lotSize);
|
|
1365
|
+
const protection = buildProtection({
|
|
1366
|
+
entry: entryPrice,
|
|
1367
|
+
direction: input.direction,
|
|
1368
|
+
size: input.size,
|
|
1369
|
+
sl: input.sl,
|
|
1370
|
+
tps: input.tps,
|
|
1371
|
+
tickSize: input.tickSize,
|
|
1372
|
+
lotSize: input.lotSize,
|
|
1373
|
+
slippagePct: input.slippagePct
|
|
1374
|
+
});
|
|
1375
|
+
const side = input.direction === "long" ? "buy" : "sell";
|
|
1376
|
+
const perp = this.perpOf(access);
|
|
1377
|
+
const enAttente = this.ciblesEnAttente(perp, {
|
|
1378
|
+
name: input.symbolXex,
|
|
1379
|
+
side,
|
|
1380
|
+
sl: protection.sl,
|
|
1381
|
+
tps: protection.tps,
|
|
1382
|
+
clientId: input.clientId
|
|
1383
|
+
});
|
|
1384
|
+
return enAttente.length === 0 ? [] : this.poserCiblesEnAttente(access, perp, input.symbolXex, side, enAttente);
|
|
1385
|
+
}
|
|
1386
|
+
/** Ce que la venue rend comme cibles non embarquées — vide chez celles qui embarquent tout. */
|
|
1387
|
+
ciblesEnAttente(perp, consigne) {
|
|
1388
|
+
return typeof perp.pendingTps === "function" ? perp.pendingTps(consigne) : [];
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Les cibles en attente, posées une par une en ordres déclenchés reduce-only — le même geste
|
|
1392
|
+
* qu'aster fait pour toutes les siennes. Rien n'est posé sans position sur la paire.
|
|
1393
|
+
*/
|
|
1394
|
+
async poserCiblesEnAttente(access, perp, symbolXex, side, enAttente) {
|
|
1395
|
+
const positions = await perp.getPositions({ name: symbolXex });
|
|
1396
|
+
const position = positions.find(
|
|
1397
|
+
(candidate) => candidate.name === symbolXex && Number(candidate.size) !== 0
|
|
1398
|
+
);
|
|
1399
|
+
if (position === void 0) {
|
|
1400
|
+
this.logger.log(
|
|
1401
|
+
`openWithProtection(${access.xex}/${symbolXex}) : pas de position, ${enAttente.length} cible(s) en attente non pos\xE9e(s).`
|
|
1402
|
+
);
|
|
1403
|
+
return [];
|
|
1404
|
+
}
|
|
1405
|
+
const exit = side === "buy" ? "sell" : "buy";
|
|
1406
|
+
const posees = [];
|
|
1407
|
+
for (const cible of enAttente) {
|
|
1408
|
+
const order = await perp.place({
|
|
1409
|
+
name: symbolXex,
|
|
1410
|
+
side: exit,
|
|
1411
|
+
type: "takeProfitMarket",
|
|
1412
|
+
size: cible.size,
|
|
1413
|
+
triggerPrice: cible.triggerPrice,
|
|
1414
|
+
price: cible.price,
|
|
1415
|
+
reduceOnly: true
|
|
1416
|
+
});
|
|
1417
|
+
posees.push(this.toOrder(order, access.xex));
|
|
1418
|
+
}
|
|
1419
|
+
this.logger.log(
|
|
1420
|
+
`openWithProtection(${access.xex}/${symbolXex}) : ${posees.length} cible(s) que la venue n'embarquait pas, pos\xE9e(s).`
|
|
1421
|
+
);
|
|
1422
|
+
return posees;
|
|
1423
|
+
}
|
|
1263
1424
|
/**
|
|
1264
1425
|
* CE QUE LA VENUE A RÉELLEMENT FAIT, après une réponse perdue.
|
|
1265
1426
|
*
|
|
@@ -2177,6 +2338,7 @@ exports.WALLET_XEXES = WALLET_XEXES;
|
|
|
2177
2338
|
exports.WS_SUBSCRIPTION_LIMITS = WS_SUBSCRIPTION_LIMITS;
|
|
2178
2339
|
exports.XGATE_EXCHANGES = XGATE_EXCHANGES;
|
|
2179
2340
|
exports.XgateEx = XgateEx;
|
|
2341
|
+
exports.assetClassOf = assetClassOf;
|
|
2180
2342
|
exports.buildLevels = buildLevels;
|
|
2181
2343
|
exports.buildProtection = buildProtection;
|
|
2182
2344
|
exports.candlesMaxHistoryOf = candlesMaxHistoryOf;
|