@blackcube/xgate-sdk 0.56.3 → 0.58.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/dist/index.cjs CHANGED
@@ -1252,8 +1252,11 @@ exports.TradingService = class TradingService {
1252
1252
  /**
1253
1253
  * OUVRE UNE POSITION AVEC SA PROTECTION, en un geste atomique.
1254
1254
  *
1255
- * Le premier ordre rendu est **l'entrée** ; les suivants sont le stop et les take-profits, dans
1256
- * l'ordre où ils ont été posés.
1255
+ * **Ce qui revient est la liste des ORDRES DU TRADE** ({@link ITradeOrder}), la même sur toutes les
1256
+ * venues : l'entrée, le stop, puis chaque take-profit dans l'ordre du plan — chacun avec son rôle, sa
1257
+ * part et un identifiant réel, retrouvé au carnet quand la venue ne l'a pas rendu à l'envoi (0.58.0,
1258
+ * cf. `ordresDuTrade`). L'appelant la garde avec son trade : annuler une protection, c'est
1259
+ * {@link cancel} sur son identifiant, quel que soit le rôle.
1257
1260
  *
1258
1261
  * Chaque venue applique son mécanisme natif — hyperliquid groupe les enfants sous l'entrée et les
1259
1262
  * annule lui-même si elle rate, pacifica les embarque dans l'ordre, aster envoie un lot de
@@ -1316,27 +1319,25 @@ exports.TradingService = class TradingService {
1316
1319
  tps: protection.tps,
1317
1320
  clientId: input.clientId
1318
1321
  };
1322
+ let posees;
1319
1323
  try {
1320
1324
  const orders = await perp.createEntryWithProtection(entry, consigne);
1321
- const posees = orders.map((order) => this.toOrder(order, access.xex));
1325
+ posees = orders.map((order) => this.toOrder(order, access.xex));
1322
1326
  const enAttente = this.ciblesEnAttente(perp, consigne);
1323
- if (enAttente.length === 0) {
1324
- return posees;
1325
- }
1326
- if (tif !== "ioc" && tif !== "fok") {
1327
+ if (enAttente.length > 0 && (tif === "ioc" || tif === "fok")) {
1328
+ const complement = await this.poserCiblesEnAttente(
1329
+ access,
1330
+ perp,
1331
+ input.symbolXex,
1332
+ side,
1333
+ enAttente
1334
+ );
1335
+ posees = [...posees, ...complement];
1336
+ } else if (enAttente.length > 0) {
1327
1337
  this.logger.warn(
1328
1338
  `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
1339
  );
1330
- return posees;
1331
1340
  }
1332
- const complement = await this.poserCiblesEnAttente(
1333
- access,
1334
- perp,
1335
- input.symbolXex,
1336
- side,
1337
- enAttente
1338
- );
1339
- return [...posees, ...complement];
1340
1341
  } catch (error) {
1341
1342
  const message = error instanceof Error ? error.message : String(error);
1342
1343
  if (message.includes(ASTER_TIMEOUT) === false) {
@@ -1345,8 +1346,119 @@ exports.TradingService = class TradingService {
1345
1346
  this.logger.warn(
1346
1347
  `openWithProtection(${access.xex}) : la venue a coup\xE9 avant de r\xE9pondre. Statut inconnu \u2014 relecture de l'\xE9tat r\xE9el.`
1347
1348
  );
1348
- return await this.etatApres(access, input.symbolXex);
1349
+ posees = await this.etatApres(access, input.symbolXex);
1349
1350
  }
1351
+ return await this.ordresDuTrade(access, perp, input, side, protection, posees);
1352
+ }
1353
+ /**
1354
+ * LES ORDRES DU TRADE, rangés par rôle — la même liste sur toutes les venues.
1355
+ *
1356
+ * Chaque venue rend ses ordres à sa façon (mesuré sur testnet le 2026-10-01) : hyperliquid en lot `'na'` rend
1357
+ * tous les identifiants, en `normalTpsl` (un seul take-profit) AUCUN pour le stop et la cible (`waitingForFill`,
1358
+ * `waitingForTrigger`) ; pacifica ne rend que l'entrée, le stop et la première cible embarqués naissant au
1359
+ * carnet sans lien (`parent_id: null`) ; aster pose une à une et rend tout.
1360
+ *
1361
+ * On range donc ce qui est revenu : l'entrée est le premier ordre non reduce-only ; le stop et chaque cible
1362
+ * sont reconnus par leur TYPE et leur PRIX DE DÉCLENCHEMENT — ceux qu'XGate vient de calculer, arrondis à la
1363
+ * grille, donc ceux que la venue a posés. Ce qui manque est cherché au CARNET de la paire, par la même
1364
+ * reconnaissance — le procédé que le SDK pacifica applique déjà à ses take-profits (`placeTakeProfit`).
1365
+ *
1366
+ * Un ordre introuvable garde un identifiant VIDE : refusé, ou déjà consommé (un stop touché dans la seconde,
1367
+ * DOT le 2026-10-01). **Mais une position ouverte dont le stop est introuvable lève** : c'est la position
1368
+ * nue que ce service existe pour empêcher, et l'appelant doit le savoir tout de suite.
1369
+ */
1370
+ async ordresDuTrade(access, perp, input, side, protection, posees) {
1371
+ const exit = side === "buy" ? "sell" : "buy";
1372
+ const part = (size) => Number(size) / input.size;
1373
+ const entree = posees.find((order) => order.reduceOnly !== true) ?? null;
1374
+ const rejetee = entree !== null && entree.status === "rejected";
1375
+ const pris = /* @__PURE__ */ new Set();
1376
+ const attendus = [
1377
+ { role: "sl", leg: protection.sl },
1378
+ ...protection.tps.map((leg) => ({ role: "tp", leg }))
1379
+ ];
1380
+ const reconnaitre = (pool, role, leg) => pool.find(
1381
+ (order) => order.id !== "" && pris.has(order.id) === false && estDuRole(order.type, role) === true && (order.triggerPrice === null || memePrix(order.triggerPrice, leg.triggerPrice) === true)
1382
+ );
1383
+ let carnet = null;
1384
+ const lireCarnet = async () => {
1385
+ if (carnet === null) {
1386
+ carnet = (await perp.getOpens()).map((order) => this.toOrder(order, access.xex)).filter((order) => order.symbolXex === input.symbolXex && order.reduceOnly === true);
1387
+ }
1388
+ return carnet;
1389
+ };
1390
+ const ordres = [
1391
+ entree === null ? this.ordreAbsent(
1392
+ access,
1393
+ input,
1394
+ "entry",
1395
+ side,
1396
+ "limit",
1397
+ 1,
1398
+ null,
1399
+ String(input.size),
1400
+ "other"
1401
+ ) : { ...entree, role: "entry", part: 1 }
1402
+ ];
1403
+ for (const { role, leg } of attendus) {
1404
+ let trouve = reconnaitre(posees, role, leg);
1405
+ if (trouve === void 0 && rejetee === false) {
1406
+ trouve = reconnaitre(await lireCarnet(), role, leg);
1407
+ }
1408
+ if (trouve !== void 0) {
1409
+ pris.add(trouve.id);
1410
+ ordres.push({ ...trouve, role, part: part(leg.size) });
1411
+ } else {
1412
+ ordres.push(
1413
+ this.ordreAbsent(
1414
+ access,
1415
+ input,
1416
+ role,
1417
+ exit,
1418
+ role === "sl" ? "stopMarket" : "takeProfitMarket",
1419
+ part(leg.size),
1420
+ leg.triggerPrice,
1421
+ leg.size,
1422
+ rejetee === true ? "canceled" : "other"
1423
+ )
1424
+ );
1425
+ }
1426
+ }
1427
+ const stop = ordres.find((order) => order.role === "sl");
1428
+ if (rejetee === false && stop !== void 0 && stop.id === "") {
1429
+ const positions = await perp.getPositions({ name: input.symbolXex });
1430
+ const ouverte = positions.find(
1431
+ (candidate) => candidate.name === input.symbolXex && Number(candidate.size) !== 0
1432
+ );
1433
+ if (ouverte !== void 0) {
1434
+ throw new Error(
1435
+ `openWithProtection(${access.xex}/${input.symbolXex}) : POSITION NUE \u2014 ${ouverte.size} ouvert, stop introuvable au carnet. \xC0 prot\xE9ger imm\xE9diatement.`
1436
+ );
1437
+ }
1438
+ }
1439
+ return ordres;
1440
+ }
1441
+ /** Un ordre du trade que la venue n'a pas posé, ou qui n'existe déjà plus — identifiant VIDE. */
1442
+ ordreAbsent(access, input, role, side, type, part, triggerPrice, size, status) {
1443
+ return {
1444
+ xex: access.xex,
1445
+ symbolXex: input.symbolXex,
1446
+ kind: "perp",
1447
+ id: "",
1448
+ clientId: null,
1449
+ side,
1450
+ type,
1451
+ price: null,
1452
+ triggerPrice,
1453
+ size,
1454
+ filled: "0",
1455
+ status,
1456
+ tif: null,
1457
+ reduceOnly: role !== "entry",
1458
+ placedAt: /* @__PURE__ */ new Date(),
1459
+ role,
1460
+ part
1461
+ };
1350
1462
  }
1351
1463
  /**
1352
1464
  * POSE LES CIBLES QUE LA VENUE N'A PAS EMBARQUÉES, une fois l'entrée remplie.
@@ -1591,15 +1703,25 @@ exports.TradingService = class TradingService {
1591
1703
  * take-profit peut remplir en plusieurs fois. Seul l'historique porte le prix réellement obtenu, et
1592
1704
  * c'est lui qui doit servir au calcul du résultat — sinon le PnL affiché est une fiction.
1593
1705
  *
1594
- * On retient le **dernier** ordre de sortie rempli après `openedAt` : les précédents sont les
1595
- * take-profits partiels, celui-là est la clôture.
1706
+ * On lit les **exécutions** (`trades`), jamais les ordres : le prix d'un ordre est sa LIMITE — chez
1707
+ * hyperliquid, un market est un IOC borné à mark ± glissement, et son « prix » est cette borne, pas le
1708
+ * prix obtenu (BNB, 2026-10-01 : sortie lue à 844,44 pour des fills à 767,69). Le prix de sortie est la
1709
+ * moyenne des exécutions de sortie après `openedAt` — et avant `until` quand il est donné — pondérée par
1710
+ * leur taille : TP partiels et clôture compris, c'est le prix auquel la position est réellement sortie.
1596
1711
  */
1597
- async exitPrice(access, symbolXex, direction, openedAt) {
1712
+ async exitPrice(access, symbolXex, direction, openedAt, until) {
1598
1713
  const exit = direction === "long" ? "sell" : "buy";
1599
- const history = await this.perpOf(access).getHistory({ name: symbolXex });
1600
- const exits = history.filter((order) => order.name === symbolXex && order.side === exit).filter((order) => order.status === "filled" || order.status === "partiallyFilled").filter((order) => order.placedAt.getTime() > openedAt.getTime()).filter((order) => order.price !== null && Number(order.price) > 0).sort((left, right) => right.placedAt.getTime() - left.placedAt.getTime());
1601
- const last = exits[0];
1602
- return last === void 0 ? null : last.price;
1714
+ const fin = until === void 0 ? Number.POSITIVE_INFINITY : until.getTime();
1715
+ const fills = (await this.trades(access)).filter((fill) => fill.symbolXex === symbolXex && fill.side === exit).filter(
1716
+ (fill) => fill.filledAt.getTime() > openedAt.getTime() && fill.filledAt.getTime() <= fin
1717
+ ).filter((fill) => Number(fill.price) > 0 && Number(fill.size) > 0);
1718
+ const taille = fills.reduce((total, fill) => total + Number(fill.size), 0);
1719
+ if (taille === 0) {
1720
+ return null;
1721
+ }
1722
+ const moyenne = fills.reduce((total, fill) => total + Number(fill.price) * Number(fill.size), 0) / taille;
1723
+ const decimales = Math.max(...fills.map((fill) => (fill.price.split(".")[1] ?? "").length));
1724
+ return String(Number(moyenne.toFixed(decimales)));
1603
1725
  }
1604
1726
  /**
1605
1727
  * L'HISTORIQUE DES ORDRES du compte — ce qui a été soumis, rempli, annulé ou expiré.
@@ -1872,6 +1994,7 @@ exports.TradingService = class TradingService {
1872
1994
  side: order.side,
1873
1995
  type: order.type,
1874
1996
  price: order.price,
1997
+ triggerPrice: order.triggerPrice,
1875
1998
  size: order.size,
1876
1999
  filled: order.filled,
1877
2000
  status: this.toStatus(order.status),
@@ -1885,6 +2008,14 @@ exports.TradingService = class TradingService {
1885
2008
  exports.TradingService = __decorateClass([
1886
2009
  common.Injectable()
1887
2010
  ], exports.TradingService);
2011
+ function estDuRole(type, role) {
2012
+ return role === "sl" ? type === "stop" || type === "stopMarket" : type === "takeProfit" || type === "takeProfitMarket";
2013
+ }
2014
+ function memePrix(lu, pose) {
2015
+ const a = Number(lu);
2016
+ const b = Number(pose);
2017
+ return Math.abs(a - b) <= 1e-9 * Math.max(1, Math.abs(b));
2018
+ }
1888
2019
  function toXexDate(date) {
1889
2020
  return date.toISOString().slice(0, 19).replace("T", " ");
1890
2021
  }
@@ -2263,6 +2394,7 @@ exports.WsTradesService = class WsTradesService {
2263
2394
  side: order.side,
2264
2395
  type: order.type,
2265
2396
  price: order.price,
2397
+ triggerPrice: order.triggerPrice,
2266
2398
  size: order.size,
2267
2399
  filled: order.filled,
2268
2400
  status: ORDER_STATUSES.includes(order.status) ? order.status : "other",