@blackcube/xgate-sdk 0.56.0 → 0.56.2
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 +112 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -1
- package/dist/index.d.ts +40 -1
- package/dist/index.js +112 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -1490,7 +1490,11 @@ interface IEntryWithProtection {
|
|
|
1490
1490
|
direction: Direction;
|
|
1491
1491
|
/** Taille à ouvrir, **en unités de base**. */
|
|
1492
1492
|
size: number;
|
|
1493
|
-
/**
|
|
1493
|
+
/**
|
|
1494
|
+
* Prix d'entrée visé. Sert de référence aux pourcentages, et de prix limite — ARRONDI à la grille
|
|
1495
|
+
* de la paire par XGate (`tickSize`, sinon la règle hyperliquid), comme les protections. Jusqu'en
|
|
1496
|
+
* 0.56.0 il partait tel quel, et pacifica refuse un prix hors tick.
|
|
1497
|
+
*/
|
|
1494
1498
|
entry: number;
|
|
1495
1499
|
/**
|
|
1496
1500
|
* LE STOP — en **écart** au prix d'entrée ou en **prix absolu**, jamais les deux.
|
|
@@ -1564,8 +1568,43 @@ declare class TradingService {
|
|
|
1564
1568
|
* Chaque venue applique son mécanisme natif — hyperliquid groupe les enfants sous l'entrée et les
|
|
1565
1569
|
* annule lui-même si elle rate, pacifica les embarque dans l'ordre, aster envoie un lot de
|
|
1566
1570
|
* conditionnels. Le SDK de la venue s'en charge ; xgate fournit des niveaux justes.
|
|
1571
|
+
*
|
|
1572
|
+
* **LE PRIX D'ENTRÉE EST ARRONDI À LA GRILLE DE LA PAIRE, comme les protections.** Jusqu'en 0.56.0 il
|
|
1573
|
+
* partait tel quel (`String(input.entry)`) : les protections étaient justes, l'entrée non — et
|
|
1574
|
+
* pacifica l'envoie au wire sans le reformater (hyperliquid, lui, le reformate). Mesuré le
|
|
1575
|
+
* 2026-09-12 en rejouant 171 ouvertures : 79 entrées hors grille (`13.7421` sur un marché à cinq
|
|
1576
|
+
* chiffres significatifs, `75.885` sur un tick de 0,01). Les niveaux en pourcentage se calculent
|
|
1577
|
+
* depuis l'entrée ARRONDIE — celle qui sera réellement posée.
|
|
1578
|
+
*
|
|
1579
|
+
* **LES CIBLES QUE LA VENUE N'EMBARQUE PAS SONT POSÉES ICI.** Pacifica n'embarque qu'un take-profit
|
|
1580
|
+
* dans l'ordre d'entrée et rend les suivants par `pendingTps()` — « à appeler une fois le fill
|
|
1581
|
+
* constaté ». Avec une entrée immédiate (`ioc`, `fok`), le fill est constaté tout de suite : on
|
|
1582
|
+
* relit la position et on pose ce qui manque, en ordres déclenchés reduce-only. Avec une entrée
|
|
1583
|
+
* qui peut rester au carnet (`gtc`, `alo`), rien ne peut être posé avant le fill : l'appelant
|
|
1584
|
+
* complète par {@link completeProtection} quand il le constate. Jusqu'en 0.56.0 ces cibles n'étaient
|
|
1585
|
+
* posées par personne : une position tide à trois paliers sur pacifica partait sans sa dernière.
|
|
1567
1586
|
*/
|
|
1568
1587
|
openWithProtection(access: ITradingAccess, input: IEntryWithProtection): Promise<IOrder[]>;
|
|
1588
|
+
/**
|
|
1589
|
+
* POSE LES CIBLES QUE LA VENUE N'A PAS EMBARQUÉES, une fois l'entrée remplie.
|
|
1590
|
+
*
|
|
1591
|
+
* Le pendant de {@link openWithProtection} pour une entrée qui pouvait rester au carnet (`gtc`,
|
|
1592
|
+
* `alo`) : l'appelant constate le fill — une position sur la paire — et complète. Les niveaux se
|
|
1593
|
+
* recalculent depuis la même consigne, donc les mêmes prix ; la venue qui embarque toutes ses
|
|
1594
|
+
* cibles (hyperliquid, aster) n'a rien en attente et rien n'est posé. Sans position, rien non plus :
|
|
1595
|
+
* un ordre reduce-only sans position serait refusé ou annulé par la venue.
|
|
1596
|
+
*
|
|
1597
|
+
* ⚠️ NON IDEMPOTENT : appelé deux fois après le même fill, il pose les cibles deux fois. L'appelant
|
|
1598
|
+
* le fait UNE fois, quand sa relecture montre la position.
|
|
1599
|
+
*/
|
|
1600
|
+
completeProtection(access: ITradingAccess, input: IEntryWithProtection): Promise<IOrder[]>;
|
|
1601
|
+
/** Ce que la venue rend comme cibles non embarquées — vide chez celles qui embarquent tout. */
|
|
1602
|
+
private ciblesEnAttente;
|
|
1603
|
+
/**
|
|
1604
|
+
* Les cibles en attente, posées une par une en ordres déclenchés reduce-only — le même geste
|
|
1605
|
+
* qu'aster fait pour toutes les siennes. Rien n'est posé sans position sur la paire.
|
|
1606
|
+
*/
|
|
1607
|
+
private poserCiblesEnAttente;
|
|
1569
1608
|
/**
|
|
1570
1609
|
* CE QUE LA VENUE A RÉELLEMENT FAIT, après une réponse perdue.
|
|
1571
1610
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -1490,7 +1490,11 @@ interface IEntryWithProtection {
|
|
|
1490
1490
|
direction: Direction;
|
|
1491
1491
|
/** Taille à ouvrir, **en unités de base**. */
|
|
1492
1492
|
size: number;
|
|
1493
|
-
/**
|
|
1493
|
+
/**
|
|
1494
|
+
* Prix d'entrée visé. Sert de référence aux pourcentages, et de prix limite — ARRONDI à la grille
|
|
1495
|
+
* de la paire par XGate (`tickSize`, sinon la règle hyperliquid), comme les protections. Jusqu'en
|
|
1496
|
+
* 0.56.0 il partait tel quel, et pacifica refuse un prix hors tick.
|
|
1497
|
+
*/
|
|
1494
1498
|
entry: number;
|
|
1495
1499
|
/**
|
|
1496
1500
|
* LE STOP — en **écart** au prix d'entrée ou en **prix absolu**, jamais les deux.
|
|
@@ -1564,8 +1568,43 @@ declare class TradingService {
|
|
|
1564
1568
|
* Chaque venue applique son mécanisme natif — hyperliquid groupe les enfants sous l'entrée et les
|
|
1565
1569
|
* annule lui-même si elle rate, pacifica les embarque dans l'ordre, aster envoie un lot de
|
|
1566
1570
|
* conditionnels. Le SDK de la venue s'en charge ; xgate fournit des niveaux justes.
|
|
1571
|
+
*
|
|
1572
|
+
* **LE PRIX D'ENTRÉE EST ARRONDI À LA GRILLE DE LA PAIRE, comme les protections.** Jusqu'en 0.56.0 il
|
|
1573
|
+
* partait tel quel (`String(input.entry)`) : les protections étaient justes, l'entrée non — et
|
|
1574
|
+
* pacifica l'envoie au wire sans le reformater (hyperliquid, lui, le reformate). Mesuré le
|
|
1575
|
+
* 2026-09-12 en rejouant 171 ouvertures : 79 entrées hors grille (`13.7421` sur un marché à cinq
|
|
1576
|
+
* chiffres significatifs, `75.885` sur un tick de 0,01). Les niveaux en pourcentage se calculent
|
|
1577
|
+
* depuis l'entrée ARRONDIE — celle qui sera réellement posée.
|
|
1578
|
+
*
|
|
1579
|
+
* **LES CIBLES QUE LA VENUE N'EMBARQUE PAS SONT POSÉES ICI.** Pacifica n'embarque qu'un take-profit
|
|
1580
|
+
* dans l'ordre d'entrée et rend les suivants par `pendingTps()` — « à appeler une fois le fill
|
|
1581
|
+
* constaté ». Avec une entrée immédiate (`ioc`, `fok`), le fill est constaté tout de suite : on
|
|
1582
|
+
* relit la position et on pose ce qui manque, en ordres déclenchés reduce-only. Avec une entrée
|
|
1583
|
+
* qui peut rester au carnet (`gtc`, `alo`), rien ne peut être posé avant le fill : l'appelant
|
|
1584
|
+
* complète par {@link completeProtection} quand il le constate. Jusqu'en 0.56.0 ces cibles n'étaient
|
|
1585
|
+
* posées par personne : une position tide à trois paliers sur pacifica partait sans sa dernière.
|
|
1567
1586
|
*/
|
|
1568
1587
|
openWithProtection(access: ITradingAccess, input: IEntryWithProtection): Promise<IOrder[]>;
|
|
1588
|
+
/**
|
|
1589
|
+
* POSE LES CIBLES QUE LA VENUE N'A PAS EMBARQUÉES, une fois l'entrée remplie.
|
|
1590
|
+
*
|
|
1591
|
+
* Le pendant de {@link openWithProtection} pour une entrée qui pouvait rester au carnet (`gtc`,
|
|
1592
|
+
* `alo`) : l'appelant constate le fill — une position sur la paire — et complète. Les niveaux se
|
|
1593
|
+
* recalculent depuis la même consigne, donc les mêmes prix ; la venue qui embarque toutes ses
|
|
1594
|
+
* cibles (hyperliquid, aster) n'a rien en attente et rien n'est posé. Sans position, rien non plus :
|
|
1595
|
+
* un ordre reduce-only sans position serait refusé ou annulé par la venue.
|
|
1596
|
+
*
|
|
1597
|
+
* ⚠️ NON IDEMPOTENT : appelé deux fois après le même fill, il pose les cibles deux fois. L'appelant
|
|
1598
|
+
* le fait UNE fois, quand sa relecture montre la position.
|
|
1599
|
+
*/
|
|
1600
|
+
completeProtection(access: ITradingAccess, input: IEntryWithProtection): Promise<IOrder[]>;
|
|
1601
|
+
/** Ce que la venue rend comme cibles non embarquées — vide chez celles qui embarquent tout. */
|
|
1602
|
+
private ciblesEnAttente;
|
|
1603
|
+
/**
|
|
1604
|
+
* Les cibles en attente, posées une par une en ordres déclenchés reduce-only — le même geste
|
|
1605
|
+
* qu'aster fait pour toutes les siennes. Rien n'est posé sans position sur la paire.
|
|
1606
|
+
*/
|
|
1607
|
+
private poserCiblesEnAttente;
|
|
1569
1608
|
/**
|
|
1570
1609
|
* CE QUE LA VENUE A RÉELLEMENT FAIT, après une réponse perdue.
|
|
1571
1610
|
*
|
package/dist/index.js
CHANGED
|
@@ -1256,6 +1256,21 @@ var TradingService = class {
|
|
|
1256
1256
|
* Chaque venue applique son mécanisme natif — hyperliquid groupe les enfants sous l'entrée et les
|
|
1257
1257
|
* annule lui-même si elle rate, pacifica les embarque dans l'ordre, aster envoie un lot de
|
|
1258
1258
|
* conditionnels. Le SDK de la venue s'en charge ; xgate fournit des niveaux justes.
|
|
1259
|
+
*
|
|
1260
|
+
* **LE PRIX D'ENTRÉE EST ARRONDI À LA GRILLE DE LA PAIRE, comme les protections.** Jusqu'en 0.56.0 il
|
|
1261
|
+
* partait tel quel (`String(input.entry)`) : les protections étaient justes, l'entrée non — et
|
|
1262
|
+
* pacifica l'envoie au wire sans le reformater (hyperliquid, lui, le reformate). Mesuré le
|
|
1263
|
+
* 2026-09-12 en rejouant 171 ouvertures : 79 entrées hors grille (`13.7421` sur un marché à cinq
|
|
1264
|
+
* chiffres significatifs, `75.885` sur un tick de 0,01). Les niveaux en pourcentage se calculent
|
|
1265
|
+
* depuis l'entrée ARRONDIE — celle qui sera réellement posée.
|
|
1266
|
+
*
|
|
1267
|
+
* **LES CIBLES QUE LA VENUE N'EMBARQUE PAS SONT POSÉES ICI.** Pacifica n'embarque qu'un take-profit
|
|
1268
|
+
* dans l'ordre d'entrée et rend les suivants par `pendingTps()` — « à appeler une fois le fill
|
|
1269
|
+
* constaté ». Avec une entrée immédiate (`ioc`, `fok`), le fill est constaté tout de suite : on
|
|
1270
|
+
* relit la position et on pose ce qui manque, en ordres déclenchés reduce-only. Avec une entrée
|
|
1271
|
+
* qui peut rester au carnet (`gtc`, `alo`), rien ne peut être posé avant le fill : l'appelant
|
|
1272
|
+
* complète par {@link completeProtection} quand il le constate. Jusqu'en 0.56.0 ces cibles n'étaient
|
|
1273
|
+
* posées par personne : une position tide à trois paliers sur pacifica partait sans sa dernière.
|
|
1259
1274
|
*/
|
|
1260
1275
|
async openWithProtection(access, input) {
|
|
1261
1276
|
if (VENUES_PROUVEES.includes(access.xex) === false) {
|
|
@@ -1263,8 +1278,9 @@ var TradingService = class {
|
|
|
1263
1278
|
`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.`
|
|
1264
1279
|
);
|
|
1265
1280
|
}
|
|
1281
|
+
const entryPrice = roundPrice(input.entry, input.tickSize, input.lotSize);
|
|
1266
1282
|
const protection = buildProtection({
|
|
1267
|
-
entry:
|
|
1283
|
+
entry: entryPrice,
|
|
1268
1284
|
direction: input.direction,
|
|
1269
1285
|
size: input.size,
|
|
1270
1286
|
sl: input.sl,
|
|
@@ -1280,13 +1296,14 @@ var TradingService = class {
|
|
|
1280
1296
|
`openWithProtection(${access.xex}) : cette venue n'expose pas l'ouverture prot\xE9g\xE9e atomique.`
|
|
1281
1297
|
);
|
|
1282
1298
|
}
|
|
1299
|
+
const tif = input.tif ?? "ioc";
|
|
1283
1300
|
const entry = {
|
|
1284
1301
|
name: input.symbolXex,
|
|
1285
1302
|
side,
|
|
1286
1303
|
type: "limit",
|
|
1287
1304
|
size: String(input.size),
|
|
1288
|
-
price: String(
|
|
1289
|
-
tif
|
|
1305
|
+
price: String(entryPrice),
|
|
1306
|
+
tif,
|
|
1290
1307
|
reduceOnly: false,
|
|
1291
1308
|
clientId: input.clientId
|
|
1292
1309
|
};
|
|
@@ -1299,7 +1316,25 @@ var TradingService = class {
|
|
|
1299
1316
|
};
|
|
1300
1317
|
try {
|
|
1301
1318
|
const orders = await perp.createEntryWithProtection(entry, consigne);
|
|
1302
|
-
|
|
1319
|
+
const posees = orders.map((order) => this.toOrder(order, access.xex));
|
|
1320
|
+
const enAttente = this.ciblesEnAttente(perp, consigne);
|
|
1321
|
+
if (enAttente.length === 0) {
|
|
1322
|
+
return posees;
|
|
1323
|
+
}
|
|
1324
|
+
if (tif !== "ioc" && tif !== "fok") {
|
|
1325
|
+
this.logger.warn(
|
|
1326
|
+
`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.`
|
|
1327
|
+
);
|
|
1328
|
+
return posees;
|
|
1329
|
+
}
|
|
1330
|
+
const complement = await this.poserCiblesEnAttente(
|
|
1331
|
+
access,
|
|
1332
|
+
perp,
|
|
1333
|
+
input.symbolXex,
|
|
1334
|
+
side,
|
|
1335
|
+
enAttente
|
|
1336
|
+
);
|
|
1337
|
+
return [...posees, ...complement];
|
|
1303
1338
|
} catch (error) {
|
|
1304
1339
|
const message = error instanceof Error ? error.message : String(error);
|
|
1305
1340
|
if (message.includes(ASTER_TIMEOUT) === false) {
|
|
@@ -1311,6 +1346,79 @@ var TradingService = class {
|
|
|
1311
1346
|
return await this.etatApres(access, input.symbolXex);
|
|
1312
1347
|
}
|
|
1313
1348
|
}
|
|
1349
|
+
/**
|
|
1350
|
+
* POSE LES CIBLES QUE LA VENUE N'A PAS EMBARQUÉES, une fois l'entrée remplie.
|
|
1351
|
+
*
|
|
1352
|
+
* Le pendant de {@link openWithProtection} pour une entrée qui pouvait rester au carnet (`gtc`,
|
|
1353
|
+
* `alo`) : l'appelant constate le fill — une position sur la paire — et complète. Les niveaux se
|
|
1354
|
+
* recalculent depuis la même consigne, donc les mêmes prix ; la venue qui embarque toutes ses
|
|
1355
|
+
* cibles (hyperliquid, aster) n'a rien en attente et rien n'est posé. Sans position, rien non plus :
|
|
1356
|
+
* un ordre reduce-only sans position serait refusé ou annulé par la venue.
|
|
1357
|
+
*
|
|
1358
|
+
* ⚠️ NON IDEMPOTENT : appelé deux fois après le même fill, il pose les cibles deux fois. L'appelant
|
|
1359
|
+
* le fait UNE fois, quand sa relecture montre la position.
|
|
1360
|
+
*/
|
|
1361
|
+
async completeProtection(access, input) {
|
|
1362
|
+
const entryPrice = roundPrice(input.entry, input.tickSize, input.lotSize);
|
|
1363
|
+
const protection = buildProtection({
|
|
1364
|
+
entry: entryPrice,
|
|
1365
|
+
direction: input.direction,
|
|
1366
|
+
size: input.size,
|
|
1367
|
+
sl: input.sl,
|
|
1368
|
+
tps: input.tps,
|
|
1369
|
+
tickSize: input.tickSize,
|
|
1370
|
+
lotSize: input.lotSize,
|
|
1371
|
+
slippagePct: input.slippagePct
|
|
1372
|
+
});
|
|
1373
|
+
const side = input.direction === "long" ? "buy" : "sell";
|
|
1374
|
+
const perp = this.perpOf(access);
|
|
1375
|
+
const enAttente = this.ciblesEnAttente(perp, {
|
|
1376
|
+
name: input.symbolXex,
|
|
1377
|
+
side,
|
|
1378
|
+
sl: protection.sl,
|
|
1379
|
+
tps: protection.tps,
|
|
1380
|
+
clientId: input.clientId
|
|
1381
|
+
});
|
|
1382
|
+
return enAttente.length === 0 ? [] : this.poserCiblesEnAttente(access, perp, input.symbolXex, side, enAttente);
|
|
1383
|
+
}
|
|
1384
|
+
/** Ce que la venue rend comme cibles non embarquées — vide chez celles qui embarquent tout. */
|
|
1385
|
+
ciblesEnAttente(perp, consigne) {
|
|
1386
|
+
return typeof perp.pendingTps === "function" ? perp.pendingTps(consigne) : [];
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Les cibles en attente, posées une par une en ordres déclenchés reduce-only — le même geste
|
|
1390
|
+
* qu'aster fait pour toutes les siennes. Rien n'est posé sans position sur la paire.
|
|
1391
|
+
*/
|
|
1392
|
+
async poserCiblesEnAttente(access, perp, symbolXex, side, enAttente) {
|
|
1393
|
+
const positions = await perp.getPositions({ name: symbolXex });
|
|
1394
|
+
const position = positions.find(
|
|
1395
|
+
(candidate) => candidate.name === symbolXex && Number(candidate.size) !== 0
|
|
1396
|
+
);
|
|
1397
|
+
if (position === void 0) {
|
|
1398
|
+
this.logger.log(
|
|
1399
|
+
`openWithProtection(${access.xex}/${symbolXex}) : pas de position, ${enAttente.length} cible(s) en attente non pos\xE9e(s).`
|
|
1400
|
+
);
|
|
1401
|
+
return [];
|
|
1402
|
+
}
|
|
1403
|
+
const exit = side === "buy" ? "sell" : "buy";
|
|
1404
|
+
const posees = [];
|
|
1405
|
+
for (const cible of enAttente) {
|
|
1406
|
+
const order = await perp.place({
|
|
1407
|
+
name: symbolXex,
|
|
1408
|
+
side: exit,
|
|
1409
|
+
type: "takeProfitMarket",
|
|
1410
|
+
size: cible.size,
|
|
1411
|
+
triggerPrice: cible.triggerPrice,
|
|
1412
|
+
price: cible.price,
|
|
1413
|
+
reduceOnly: true
|
|
1414
|
+
});
|
|
1415
|
+
posees.push(this.toOrder(order, access.xex));
|
|
1416
|
+
}
|
|
1417
|
+
this.logger.log(
|
|
1418
|
+
`openWithProtection(${access.xex}/${symbolXex}) : ${posees.length} cible(s) que la venue n'embarquait pas, pos\xE9e(s).`
|
|
1419
|
+
);
|
|
1420
|
+
return posees;
|
|
1421
|
+
}
|
|
1314
1422
|
/**
|
|
1315
1423
|
* CE QUE LA VENUE A RÉELLEMENT FAIT, après une réponse perdue.
|
|
1316
1424
|
*
|