@blackcube/xgate-sdk 0.56.0 → 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 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
@@ -1258,6 +1258,21 @@ exports.TradingService = class TradingService {
1258
1258
  * Chaque venue applique son mécanisme natif — hyperliquid groupe les enfants sous l'entrée et les
1259
1259
  * annule lui-même si elle rate, pacifica les embarque dans l'ordre, aster envoie un lot de
1260
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.
1261
1276
  */
1262
1277
  async openWithProtection(access, input) {
1263
1278
  if (VENUES_PROUVEES.includes(access.xex) === false) {
@@ -1265,8 +1280,9 @@ exports.TradingService = class TradingService {
1265
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.`
1266
1281
  );
1267
1282
  }
1283
+ const entryPrice = roundPrice(input.entry, input.tickSize, input.lotSize);
1268
1284
  const protection = buildProtection({
1269
- entry: input.entry,
1285
+ entry: entryPrice,
1270
1286
  direction: input.direction,
1271
1287
  size: input.size,
1272
1288
  sl: input.sl,
@@ -1282,13 +1298,14 @@ exports.TradingService = class TradingService {
1282
1298
  `openWithProtection(${access.xex}) : cette venue n'expose pas l'ouverture prot\xE9g\xE9e atomique.`
1283
1299
  );
1284
1300
  }
1301
+ const tif = input.tif ?? "ioc";
1285
1302
  const entry = {
1286
1303
  name: input.symbolXex,
1287
1304
  side,
1288
1305
  type: "limit",
1289
1306
  size: String(input.size),
1290
- price: String(input.entry),
1291
- tif: input.tif ?? "ioc",
1307
+ price: String(entryPrice),
1308
+ tif,
1292
1309
  reduceOnly: false,
1293
1310
  clientId: input.clientId
1294
1311
  };
@@ -1301,7 +1318,25 @@ exports.TradingService = class TradingService {
1301
1318
  };
1302
1319
  try {
1303
1320
  const orders = await perp.createEntryWithProtection(entry, consigne);
1304
- return orders.map((order) => this.toOrder(order, access.xex));
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];
1305
1340
  } catch (error) {
1306
1341
  const message = error instanceof Error ? error.message : String(error);
1307
1342
  if (message.includes(ASTER_TIMEOUT) === false) {
@@ -1313,6 +1348,79 @@ exports.TradingService = class TradingService {
1313
1348
  return await this.etatApres(access, input.symbolXex);
1314
1349
  }
1315
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
+ }
1316
1424
  /**
1317
1425
  * CE QUE LA VENUE A RÉELLEMENT FAIT, après une réponse perdue.
1318
1426
  *