@blackcube/xgate-sdk 0.56.2 → 0.58.0
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 +177 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -4
- package/dist/index.d.ts +68 -4
- package/dist/index.js +177 -18
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
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
|
-
*
|
|
1256
|
-
* l'ordre
|
|
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
|
-
|
|
1325
|
+
posees = orders.map((order) => this.toOrder(order, access.xex));
|
|
1322
1326
|
const enAttente = this.ciblesEnAttente(perp, consigne);
|
|
1323
|
-
if (enAttente.length ===
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
-
|
|
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.
|
|
@@ -1623,6 +1735,8 @@ exports.TradingService = class TradingService {
|
|
|
1623
1735
|
* disent la même chose**. La forme native complète reste dans `xtras`, rien n'est perdu.
|
|
1624
1736
|
*
|
|
1625
1737
|
* **blofin ne l'expose pas** et lève ; les sept autres le servent.
|
|
1738
|
+
*
|
|
1739
|
+
* **hyperliquid en compte unifié lit le spot** (`unifiedAccountState`) : son état perp y vaut 0.
|
|
1626
1740
|
*/
|
|
1627
1741
|
async accountInfo(access) {
|
|
1628
1742
|
const perp = this.perpOf(access);
|
|
@@ -1632,7 +1746,42 @@ exports.TradingService = class TradingService {
|
|
|
1632
1746
|
);
|
|
1633
1747
|
}
|
|
1634
1748
|
const brut = await perp.getAccountInfo();
|
|
1635
|
-
|
|
1749
|
+
const etat = this.toAccountState(brut, access.xex);
|
|
1750
|
+
return access.xex === "hyperliquid" /* Hyperliquid */ ? this.unifiedAccountState(access, etat) : etat;
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* HYPERLIQUID EN COMPTE UNIFIÉ — l'argent vit dans le spot, et l'état perp affiche 0.
|
|
1754
|
+
*
|
|
1755
|
+
* La doc HL : « unified account and portfolio margin show all balances and holds in the spot
|
|
1756
|
+
* clearinghouse state. Individual perp dex user states are not meaningful ». Lu côté perp, un
|
|
1757
|
+
* compte unifié de 101 USDC rend une équité de 0 — et tout appelant qui dimensionne une mise sur
|
|
1758
|
+
* l'équité refuse d'ouvrir (Blips, 2026-10-01 : « plafond d'engagement » sur un compte financé).
|
|
1759
|
+
*
|
|
1760
|
+
* Le mode est DEMANDÉ à HL (`getAbstraction`), jamais déduit des soldes : en mode standard
|
|
1761
|
+
* (`disabled`), le spot ne finance pas les perps, et le compter fausserait l'équité dans l'autre
|
|
1762
|
+
* sens. Hors `unifiedAccount`/`portfolioMargin`, l'état perp est rendu tel quel.
|
|
1763
|
+
*
|
|
1764
|
+
* L'équité est le total USDC du spot, le disponible ce total moins ce que HL bloque (`hold`).
|
|
1765
|
+
* **Le PnL latent n'y est pas ajouté** (décision Philippe, 2026-10-01) : la doc ne dit pas si le
|
|
1766
|
+
* total spot le contient déjà, et l'ajouter risquerait de le compter deux fois. En portfolio
|
|
1767
|
+
* margin, les autres actifs éligibles (HYPE, BTC, USDT) ne sont pas comptés non plus : seul l'USDC
|
|
1768
|
+
* finance ici. Le mode et le solde lu restent dans `xtras`.
|
|
1769
|
+
*/
|
|
1770
|
+
async unifiedAccountState(access, etat) {
|
|
1771
|
+
const source = createAccountXex(access.xex, access);
|
|
1772
|
+
const abstraction = await source.native.account().getAbstraction();
|
|
1773
|
+
if (abstraction.mode !== "unifiedAccount" && abstraction.mode !== "portfolioMargin") {
|
|
1774
|
+
return etat;
|
|
1775
|
+
}
|
|
1776
|
+
const usdc = (await source.account().getBalances()).find((balance) => balance.asset === "USDC");
|
|
1777
|
+
const total = usdc?.total ?? "0";
|
|
1778
|
+
const hold = Number(usdc?.xtras?.hold ?? 0);
|
|
1779
|
+
return {
|
|
1780
|
+
...etat,
|
|
1781
|
+
equity: total,
|
|
1782
|
+
available: String(Math.max(0, Number(total) - hold)),
|
|
1783
|
+
xtras: { ...etat.xtras, abstraction: abstraction.mode, spotUsdc: usdc ?? null }
|
|
1784
|
+
};
|
|
1636
1785
|
}
|
|
1637
1786
|
/**
|
|
1638
1787
|
* RÈGLE LE LEVIER d'une paire, avant d'ouvrir.
|
|
@@ -1835,6 +1984,7 @@ exports.TradingService = class TradingService {
|
|
|
1835
1984
|
side: order.side,
|
|
1836
1985
|
type: order.type,
|
|
1837
1986
|
price: order.price,
|
|
1987
|
+
triggerPrice: order.triggerPrice,
|
|
1838
1988
|
size: order.size,
|
|
1839
1989
|
filled: order.filled,
|
|
1840
1990
|
status: this.toStatus(order.status),
|
|
@@ -1848,6 +1998,14 @@ exports.TradingService = class TradingService {
|
|
|
1848
1998
|
exports.TradingService = __decorateClass([
|
|
1849
1999
|
common.Injectable()
|
|
1850
2000
|
], exports.TradingService);
|
|
2001
|
+
function estDuRole(type, role) {
|
|
2002
|
+
return role === "sl" ? type === "stop" || type === "stopMarket" : type === "takeProfit" || type === "takeProfitMarket";
|
|
2003
|
+
}
|
|
2004
|
+
function memePrix(lu, pose) {
|
|
2005
|
+
const a = Number(lu);
|
|
2006
|
+
const b = Number(pose);
|
|
2007
|
+
return Math.abs(a - b) <= 1e-9 * Math.max(1, Math.abs(b));
|
|
2008
|
+
}
|
|
1851
2009
|
function toXexDate(date) {
|
|
1852
2010
|
return date.toISOString().slice(0, 19).replace("T", " ");
|
|
1853
2011
|
}
|
|
@@ -2226,6 +2384,7 @@ exports.WsTradesService = class WsTradesService {
|
|
|
2226
2384
|
side: order.side,
|
|
2227
2385
|
type: order.type,
|
|
2228
2386
|
price: order.price,
|
|
2387
|
+
triggerPrice: order.triggerPrice,
|
|
2229
2388
|
size: order.size,
|
|
2230
2389
|
filled: order.filled,
|
|
2231
2390
|
status: ORDER_STATUSES.includes(order.status) ? order.status : "other",
|