@blackcube/xgate-sdk 0.22.0 → 0.24.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/README.md +344 -24
- package/dist/index.cjs +875 -155
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +452 -5
- package/dist/index.d.ts +452 -5
- package/dist/index.js +873 -156
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/index.cjs
CHANGED
|
@@ -11,6 +11,7 @@ var hyperliquidSdk = require('@blackcube/hyperliquid-sdk');
|
|
|
11
11
|
var lighterSdk = require('@blackcube/lighter-sdk');
|
|
12
12
|
var pacificaSdk = require('@blackcube/pacifica-sdk');
|
|
13
13
|
var paradexSdk = require('@blackcube/paradex-sdk');
|
|
14
|
+
var crypto = require('crypto');
|
|
14
15
|
|
|
15
16
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
16
17
|
var __decorateClass = (decorators, target, key, kind) => {
|
|
@@ -21,131 +22,6 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
21
22
|
return result;
|
|
22
23
|
};
|
|
23
24
|
|
|
24
|
-
// src/enums/timeframe.enums.ts
|
|
25
|
-
var Timeframe = /* @__PURE__ */ ((Timeframe2) => {
|
|
26
|
-
Timeframe2["M1"] = "1m";
|
|
27
|
-
Timeframe2["M5"] = "5m";
|
|
28
|
-
Timeframe2["M15"] = "15m";
|
|
29
|
-
Timeframe2["H1"] = "1h";
|
|
30
|
-
Timeframe2["H4"] = "4h";
|
|
31
|
-
Timeframe2["D"] = "1d";
|
|
32
|
-
Timeframe2["W"] = "1w";
|
|
33
|
-
return Timeframe2;
|
|
34
|
-
})(Timeframe || {});
|
|
35
|
-
var TIMEFRAME_MINUTES = {
|
|
36
|
-
["1m" /* M1 */]: 1,
|
|
37
|
-
["5m" /* M5 */]: 5,
|
|
38
|
-
["15m" /* M15 */]: 15,
|
|
39
|
-
["1h" /* H1 */]: 60,
|
|
40
|
-
["4h" /* H4 */]: 240,
|
|
41
|
-
["1d" /* D */]: 1440,
|
|
42
|
-
["1w" /* W */]: 10080
|
|
43
|
-
};
|
|
44
|
-
function timeframeMs(timeframe) {
|
|
45
|
-
return TIMEFRAME_MINUTES[timeframe] * 6e4;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// src/helpers/decimal.ts
|
|
49
|
-
function parse(value) {
|
|
50
|
-
const trimmed = value.trim();
|
|
51
|
-
if (/^-?\d+(\.\d+)?$/.test(trimmed) === false) {
|
|
52
|
-
throw new Error(`D\xE9cimal illisible : "${value}".`);
|
|
53
|
-
}
|
|
54
|
-
const dot = trimmed.indexOf(".");
|
|
55
|
-
if (dot < 0) {
|
|
56
|
-
return { digits: BigInt(trimmed), scale: 0 };
|
|
57
|
-
}
|
|
58
|
-
return {
|
|
59
|
-
digits: BigInt(trimmed.slice(0, dot) + trimmed.slice(dot + 1)),
|
|
60
|
-
scale: trimmed.length - dot - 1
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
function format(digits, scale) {
|
|
64
|
-
const negative = digits < 0n;
|
|
65
|
-
const plain = (negative === true ? -digits : digits).toString().padStart(scale + 1, "0");
|
|
66
|
-
const sign = negative === true ? "-" : "";
|
|
67
|
-
if (scale === 0) {
|
|
68
|
-
return sign + plain;
|
|
69
|
-
}
|
|
70
|
-
const whole = plain.slice(0, plain.length - scale);
|
|
71
|
-
const fraction = plain.slice(plain.length - scale).replace(/0+$/, "");
|
|
72
|
-
return fraction === "" ? sign + whole : `${sign + whole}.${fraction}`;
|
|
73
|
-
}
|
|
74
|
-
function addDecimals(left, right) {
|
|
75
|
-
const a = parse(left);
|
|
76
|
-
const b = parse(right);
|
|
77
|
-
const scale = Math.max(a.scale, b.scale);
|
|
78
|
-
const digits = a.digits * 10n ** BigInt(scale - a.scale) + b.digits * 10n ** BigInt(scale - b.scale);
|
|
79
|
-
return format(digits, scale);
|
|
80
|
-
}
|
|
81
|
-
function compareDecimals(left, right) {
|
|
82
|
-
const a = parse(left);
|
|
83
|
-
const b = parse(right);
|
|
84
|
-
const scale = Math.max(a.scale, b.scale);
|
|
85
|
-
const x = a.digits * 10n ** BigInt(scale - a.scale);
|
|
86
|
-
const y = b.digits * 10n ** BigInt(scale - b.scale);
|
|
87
|
-
if (x === y) {
|
|
88
|
-
return 0;
|
|
89
|
-
}
|
|
90
|
-
return x > y ? 1 : -1;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// src/helpers/candle-aggregator.ts
|
|
94
|
-
var MONDAY_OFFSET_MS = 4 * 24 * 60 * 60 * 1e3;
|
|
95
|
-
function aggregate(source, target, now = Date.now()) {
|
|
96
|
-
if (source.length === 0) {
|
|
97
|
-
return [];
|
|
98
|
-
}
|
|
99
|
-
const sourceMs = timeframeMs(source[0]?.interval ?? "1m" /* M1 */);
|
|
100
|
-
const targetMs = timeframeMs(target);
|
|
101
|
-
if (targetMs <= sourceMs) {
|
|
102
|
-
return [];
|
|
103
|
-
}
|
|
104
|
-
const offsetMs = target === "1w" /* W */ ? MONDAY_OFFSET_MS : 0;
|
|
105
|
-
const expected = targetMs / sourceMs;
|
|
106
|
-
const buckets = /* @__PURE__ */ new Map();
|
|
107
|
-
const sorted = [...source].sort(
|
|
108
|
-
(left, right) => left.openedAt.getTime() - right.openedAt.getTime()
|
|
109
|
-
);
|
|
110
|
-
for (const candle of sorted) {
|
|
111
|
-
const start = Math.floor((candle.openedAt.getTime() - offsetMs) / targetMs) * targetMs + offsetMs;
|
|
112
|
-
const bucket = buckets.get(start);
|
|
113
|
-
if (bucket === void 0) {
|
|
114
|
-
buckets.set(start, {
|
|
115
|
-
sources: 1,
|
|
116
|
-
candle: {
|
|
117
|
-
...candle,
|
|
118
|
-
interval: target,
|
|
119
|
-
openedAt: new Date(start),
|
|
120
|
-
// Dernière milliseconde du créneau, comme les bougies servies par les venues.
|
|
121
|
-
closedAt: new Date(start + targetMs - 1),
|
|
122
|
-
derived: true
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
} else {
|
|
126
|
-
bucket.sources += 1;
|
|
127
|
-
const built = bucket.candle;
|
|
128
|
-
built.high = compareDecimals(candle.high, built.high) > 0 ? candle.high : built.high;
|
|
129
|
-
built.low = compareDecimals(candle.low, built.low) < 0 ? candle.low : built.low;
|
|
130
|
-
built.close = candle.close;
|
|
131
|
-
built.volume = sum(built.volume, candle.volume);
|
|
132
|
-
built.quoteVolume = sum(built.quoteVolume, candle.quoteVolume);
|
|
133
|
-
built.takerBuyQuoteVolume = sum(built.takerBuyQuoteVolume, candle.takerBuyQuoteVolume);
|
|
134
|
-
built.trades = built.trades === null || candle.trades === null ? null : built.trades + candle.trades;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return [...buckets.keys()].sort((left, right) => left - right).map((start) => buckets.get(start)).filter((bucket) => bucket !== void 0).filter((bucket) => {
|
|
138
|
-
const stillOpen = bucket.candle.openedAt.getTime() + targetMs > now;
|
|
139
|
-
return bucket.sources >= expected || stillOpen === true;
|
|
140
|
-
}).map((bucket) => bucket.candle);
|
|
141
|
-
}
|
|
142
|
-
function sum(left, right) {
|
|
143
|
-
if (left === null || right === null) {
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
return addDecimals(left, right);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
25
|
// src/helpers/symbol-naming.ts
|
|
150
26
|
var CONCAT_QUOTE_XEXS = /* @__PURE__ */ new Set(["aster", "binance", "bybit"]);
|
|
151
27
|
var K_MULTIPLIER = /^k(?=[A-Z])/u;
|
|
@@ -221,15 +97,13 @@ function toCandle(wire, xex, interval, quote) {
|
|
|
221
97
|
};
|
|
222
98
|
}
|
|
223
99
|
|
|
224
|
-
// src/helpers/
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
limit: query.limit
|
|
232
|
-
};
|
|
100
|
+
// src/helpers/ws-limits.ts
|
|
101
|
+
var WS_SUBSCRIPTION_LIMITS = {
|
|
102
|
+
aster: 200,
|
|
103
|
+
hyperliquid: 1e3
|
|
104
|
+
};
|
|
105
|
+
function subscriptionLimitOf(xex) {
|
|
106
|
+
return WS_SUBSCRIPTION_LIMITS[xex];
|
|
233
107
|
}
|
|
234
108
|
|
|
235
109
|
// src/enums/xgate-ex.enums.ts
|
|
@@ -377,6 +251,282 @@ function createPublicSpotXex(xex) {
|
|
|
377
251
|
}
|
|
378
252
|
}
|
|
379
253
|
|
|
254
|
+
// src/services/candles-stream.service.ts
|
|
255
|
+
var CandlesStreamService = class {
|
|
256
|
+
constructor(xex, kind = "perp") {
|
|
257
|
+
this.xex = xex;
|
|
258
|
+
this.kind = kind;
|
|
259
|
+
this.logger = new common.Logger(`CandlesStream:${xex}${kind === "spot" ? ":spot" : ""}`);
|
|
260
|
+
this.maxSubscriptions = subscriptionLimitOf(xex) ?? null;
|
|
261
|
+
}
|
|
262
|
+
xex;
|
|
263
|
+
kind;
|
|
264
|
+
logger;
|
|
265
|
+
source = null;
|
|
266
|
+
unsubscribes = [];
|
|
267
|
+
/** Le plafond de souscriptions documenté par la venue, ou `null` si personne ne l'a publié. */
|
|
268
|
+
maxSubscriptions;
|
|
269
|
+
/**
|
|
270
|
+
* Ouvre le flux sur une LISTE de marchés, sur une seule socket.
|
|
271
|
+
*
|
|
272
|
+
* **UNE ERREUR DE FLUX COUPE TOUT, PUIS LÈVE.** Elle n'arrive pas à la souscription : mesuré le
|
|
273
|
+
* 2026-08-07, la venue accepte un symbole inconnu sans broncher, puis ferme la connexion en le
|
|
274
|
+
* découvrant. Le SDK reconnecte, elle referme — cinq cycles en douze secondes, et sur une socket
|
|
275
|
+
* partagée **tous** les marchés valides cessent de recevoir, en silence.
|
|
276
|
+
*
|
|
277
|
+
* On refuse cette boucle : à la première erreur, les souscriptions sont coupées, l'incident est
|
|
278
|
+
* journalisé en `error`, et l'exception part. Un flux à moitié mort qui se tait coûte plus cher
|
|
279
|
+
* qu'un arrêt franc — on ne découvre le premier qu'en constatant l'absence de données.
|
|
280
|
+
*
|
|
281
|
+
* ⚠️ L'exception naît dans un rappel de socket : elle ne remonte pas à l'appelant de `start()`,
|
|
282
|
+
* elle sort en erreur non capturée. C'est délibéré — elle doit être impossible à ignorer.
|
|
283
|
+
*
|
|
284
|
+
* **binance et bybit ne remontent pas encore leurs erreurs** : leur client gère la fermeture en
|
|
285
|
+
* interne sans l'exposer. Sur ces deux venues, la boucle silencieuse reste possible.
|
|
286
|
+
*/
|
|
287
|
+
start(symbolsXex, interval, onCandle) {
|
|
288
|
+
const ws = this.wire();
|
|
289
|
+
if ("onError" in ws) {
|
|
290
|
+
ws.onError = (error) => {
|
|
291
|
+
const raison = lisible(error);
|
|
292
|
+
this.logger.error(`${this.xex} : erreur de flux \u2014 ${raison}. Souscriptions coup\xE9es.`);
|
|
293
|
+
this.stop();
|
|
294
|
+
throw new Error(`stream(${this.xex}) : flux interrompu \u2014 ${raison}`);
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
for (const symbolXex of symbolsXex) {
|
|
298
|
+
this.unsubscribes.push(
|
|
299
|
+
ws.subscribeCandles({ name: symbolXex, interval }, (wire) => {
|
|
300
|
+
onCandle(toCandle(wire, this.xex, interval, this.quoteOf(symbolXex)));
|
|
301
|
+
})
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
this.logger.log(`${this.unsubscribes.length}/${symbolsXex.length} souscription(s) ouvertes`);
|
|
305
|
+
}
|
|
306
|
+
/** Coupe tout. Un désabonnement qui échoue ne doit pas empêcher les autres de se fermer. */
|
|
307
|
+
stop() {
|
|
308
|
+
for (const unsubscribe of this.unsubscribes.splice(0)) {
|
|
309
|
+
try {
|
|
310
|
+
unsubscribe();
|
|
311
|
+
} catch {
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/** Le nombre de souscriptions ouvertes. La liste des désabonnements EST le compteur. */
|
|
316
|
+
subscriptionCount() {
|
|
317
|
+
return this.unsubscribes.length;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Le client temps réel, sur le bon marché.
|
|
321
|
+
*
|
|
322
|
+
* Les deux contrats ne signent pas `ws()` pareil, et c'est voulu : côté comptant le marché est
|
|
323
|
+
* **obligatoire** (`ws('spot')`), parce que les façades ouvrent du perpétuel par défaut — un
|
|
324
|
+
* `ws()` nu y diffuserait des prix de perpétuel à qui croit écouter le comptant.
|
|
325
|
+
*/
|
|
326
|
+
wire() {
|
|
327
|
+
const source = this.instance();
|
|
328
|
+
return this.kind === "spot" ? source.ws("spot") : source.ws();
|
|
329
|
+
}
|
|
330
|
+
/** La façade de la venue, créée une seule fois : c'est elle qui porte la socket partagée. */
|
|
331
|
+
instance() {
|
|
332
|
+
if (this.source === null) {
|
|
333
|
+
if (this.kind === "spot" && servesSpot(this.xex) === false) {
|
|
334
|
+
throw new Error(`stream(${this.xex}) : cette venue ne sert pas de comptant.`);
|
|
335
|
+
}
|
|
336
|
+
this.source = this.kind === "spot" ? createPublicSpotXex(this.xex) : createPublicXex(this.xex);
|
|
337
|
+
}
|
|
338
|
+
return this.source;
|
|
339
|
+
}
|
|
340
|
+
/** Voir `CandlesService.quoteOf` : la cotation se déduit du symbole natif, faute d'être publiée. */
|
|
341
|
+
quoteOf(symbolXex) {
|
|
342
|
+
const parts = symbolXex.split("-");
|
|
343
|
+
const last = parts[parts.length - 1];
|
|
344
|
+
return parts.length > 1 && last !== void 0 ? last : "";
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
exports.CandlesStreamRegistry = class CandlesStreamRegistry {
|
|
348
|
+
streams = /* @__PURE__ */ new Map();
|
|
349
|
+
/** Le flux d'une venue, créé au premier appel puis réutilisé. */
|
|
350
|
+
of(xex, kind = "perp") {
|
|
351
|
+
const cle = `${xex}:${kind}`;
|
|
352
|
+
let stream = this.streams.get(cle);
|
|
353
|
+
if (stream === void 0) {
|
|
354
|
+
stream = new CandlesStreamService(xex, kind);
|
|
355
|
+
this.streams.set(cle, stream);
|
|
356
|
+
}
|
|
357
|
+
return stream;
|
|
358
|
+
}
|
|
359
|
+
/** Coupe tous les flux ouverts, toutes venues confondues. */
|
|
360
|
+
stopAll() {
|
|
361
|
+
for (const stream of this.streams.values()) {
|
|
362
|
+
stream.stop();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/** Ce qui est ouvert, par venue — pour surveiller sans avoir à tenir de compteur soi-même. */
|
|
366
|
+
counts() {
|
|
367
|
+
const total = {};
|
|
368
|
+
for (const [cle, stream] of this.streams) {
|
|
369
|
+
total[cle] = stream.subscriptionCount();
|
|
370
|
+
}
|
|
371
|
+
return total;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
exports.CandlesStreamRegistry = __decorateClass([
|
|
375
|
+
common.Injectable()
|
|
376
|
+
], exports.CandlesStreamRegistry);
|
|
377
|
+
function lisible(error) {
|
|
378
|
+
if (error instanceof Error) {
|
|
379
|
+
return error.message;
|
|
380
|
+
}
|
|
381
|
+
const event = error;
|
|
382
|
+
if (event !== null && typeof event === "object") {
|
|
383
|
+
const message = event.message ?? event.error?.message;
|
|
384
|
+
if (typeof message === "string" && message !== "") {
|
|
385
|
+
return message;
|
|
386
|
+
}
|
|
387
|
+
if (typeof event.type === "string" && event.type !== "") {
|
|
388
|
+
return `\xE9v\xE9nement \xAB ${event.type} \xBB`;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return String(error);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/enums/timeframe.enums.ts
|
|
395
|
+
var Timeframe = /* @__PURE__ */ ((Timeframe2) => {
|
|
396
|
+
Timeframe2["M1"] = "1m";
|
|
397
|
+
Timeframe2["M5"] = "5m";
|
|
398
|
+
Timeframe2["M15"] = "15m";
|
|
399
|
+
Timeframe2["H1"] = "1h";
|
|
400
|
+
Timeframe2["H4"] = "4h";
|
|
401
|
+
Timeframe2["D"] = "1d";
|
|
402
|
+
Timeframe2["W"] = "1w";
|
|
403
|
+
return Timeframe2;
|
|
404
|
+
})(Timeframe || {});
|
|
405
|
+
var TIMEFRAME_MINUTES = {
|
|
406
|
+
["1m" /* M1 */]: 1,
|
|
407
|
+
["5m" /* M5 */]: 5,
|
|
408
|
+
["15m" /* M15 */]: 15,
|
|
409
|
+
["1h" /* H1 */]: 60,
|
|
410
|
+
["4h" /* H4 */]: 240,
|
|
411
|
+
["1d" /* D */]: 1440,
|
|
412
|
+
["1w" /* W */]: 10080
|
|
413
|
+
};
|
|
414
|
+
function timeframeMs(timeframe) {
|
|
415
|
+
return TIMEFRAME_MINUTES[timeframe] * 6e4;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/helpers/decimal.ts
|
|
419
|
+
function parse(value) {
|
|
420
|
+
const trimmed = value.trim();
|
|
421
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed) === false) {
|
|
422
|
+
throw new Error(`D\xE9cimal illisible : "${value}".`);
|
|
423
|
+
}
|
|
424
|
+
const dot = trimmed.indexOf(".");
|
|
425
|
+
if (dot < 0) {
|
|
426
|
+
return { digits: BigInt(trimmed), scale: 0 };
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
digits: BigInt(trimmed.slice(0, dot) + trimmed.slice(dot + 1)),
|
|
430
|
+
scale: trimmed.length - dot - 1
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function format(digits, scale) {
|
|
434
|
+
const negative = digits < 0n;
|
|
435
|
+
const plain = (negative === true ? -digits : digits).toString().padStart(scale + 1, "0");
|
|
436
|
+
const sign = negative === true ? "-" : "";
|
|
437
|
+
if (scale === 0) {
|
|
438
|
+
return sign + plain;
|
|
439
|
+
}
|
|
440
|
+
const whole = plain.slice(0, plain.length - scale);
|
|
441
|
+
const fraction = plain.slice(plain.length - scale).replace(/0+$/, "");
|
|
442
|
+
return fraction === "" ? sign + whole : `${sign + whole}.${fraction}`;
|
|
443
|
+
}
|
|
444
|
+
function addDecimals(left, right) {
|
|
445
|
+
const a = parse(left);
|
|
446
|
+
const b = parse(right);
|
|
447
|
+
const scale = Math.max(a.scale, b.scale);
|
|
448
|
+
const digits = a.digits * 10n ** BigInt(scale - a.scale) + b.digits * 10n ** BigInt(scale - b.scale);
|
|
449
|
+
return format(digits, scale);
|
|
450
|
+
}
|
|
451
|
+
function compareDecimals(left, right) {
|
|
452
|
+
const a = parse(left);
|
|
453
|
+
const b = parse(right);
|
|
454
|
+
const scale = Math.max(a.scale, b.scale);
|
|
455
|
+
const x = a.digits * 10n ** BigInt(scale - a.scale);
|
|
456
|
+
const y = b.digits * 10n ** BigInt(scale - b.scale);
|
|
457
|
+
if (x === y) {
|
|
458
|
+
return 0;
|
|
459
|
+
}
|
|
460
|
+
return x > y ? 1 : -1;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/helpers/candle-aggregator.ts
|
|
464
|
+
var MONDAY_OFFSET_MS = 4 * 24 * 60 * 60 * 1e3;
|
|
465
|
+
function aggregate(source, target, now = Date.now()) {
|
|
466
|
+
if (source.length === 0) {
|
|
467
|
+
return [];
|
|
468
|
+
}
|
|
469
|
+
const sourceMs = timeframeMs(source[0]?.interval ?? "1m" /* M1 */);
|
|
470
|
+
const targetMs = timeframeMs(target);
|
|
471
|
+
if (targetMs <= sourceMs) {
|
|
472
|
+
return [];
|
|
473
|
+
}
|
|
474
|
+
const offsetMs = target === "1w" /* W */ ? MONDAY_OFFSET_MS : 0;
|
|
475
|
+
const expected = targetMs / sourceMs;
|
|
476
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
477
|
+
const sorted = [...source].sort(
|
|
478
|
+
(left, right) => left.openedAt.getTime() - right.openedAt.getTime()
|
|
479
|
+
);
|
|
480
|
+
for (const candle of sorted) {
|
|
481
|
+
const start = Math.floor((candle.openedAt.getTime() - offsetMs) / targetMs) * targetMs + offsetMs;
|
|
482
|
+
const bucket = buckets.get(start);
|
|
483
|
+
if (bucket === void 0) {
|
|
484
|
+
buckets.set(start, {
|
|
485
|
+
sources: 1,
|
|
486
|
+
candle: {
|
|
487
|
+
...candle,
|
|
488
|
+
interval: target,
|
|
489
|
+
openedAt: new Date(start),
|
|
490
|
+
// Dernière milliseconde du créneau, comme les bougies servies par les venues.
|
|
491
|
+
closedAt: new Date(start + targetMs - 1),
|
|
492
|
+
derived: true
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
} else {
|
|
496
|
+
bucket.sources += 1;
|
|
497
|
+
const built = bucket.candle;
|
|
498
|
+
built.high = compareDecimals(candle.high, built.high) > 0 ? candle.high : built.high;
|
|
499
|
+
built.low = compareDecimals(candle.low, built.low) < 0 ? candle.low : built.low;
|
|
500
|
+
built.close = candle.close;
|
|
501
|
+
built.volume = sum(built.volume, candle.volume);
|
|
502
|
+
built.quoteVolume = sum(built.quoteVolume, candle.quoteVolume);
|
|
503
|
+
built.takerBuyQuoteVolume = sum(built.takerBuyQuoteVolume, candle.takerBuyQuoteVolume);
|
|
504
|
+
built.trades = built.trades === null || candle.trades === null ? null : built.trades + candle.trades;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return [...buckets.keys()].sort((left, right) => left - right).map((start) => buckets.get(start)).filter((bucket) => bucket !== void 0).filter((bucket) => {
|
|
508
|
+
const stillOpen = bucket.candle.openedAt.getTime() + targetMs > now;
|
|
509
|
+
return bucket.sources >= expected || stillOpen === true;
|
|
510
|
+
}).map((bucket) => bucket.candle);
|
|
511
|
+
}
|
|
512
|
+
function sum(left, right) {
|
|
513
|
+
if (left === null || right === null) {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
return addDecimals(left, right);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/helpers/candle-query.ts
|
|
520
|
+
function toXexQuery(query) {
|
|
521
|
+
return {
|
|
522
|
+
name: query.symbol,
|
|
523
|
+
interval: query.interval,
|
|
524
|
+
startTime: query.startTime,
|
|
525
|
+
endTime: query.endTime,
|
|
526
|
+
limit: query.limit
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
380
530
|
// src/helpers/xex-timeframes.ts
|
|
381
531
|
var HYPERLIQUID_NATIVE = Object.values(Timeframe).filter(
|
|
382
532
|
(interval) => interval !== "1w" /* W */
|
|
@@ -795,9 +945,136 @@ exports.SpotCatalogService = class SpotCatalogService {
|
|
|
795
945
|
}));
|
|
796
946
|
}
|
|
797
947
|
};
|
|
798
|
-
exports.SpotCatalogService = __decorateClass([
|
|
948
|
+
exports.SpotCatalogService = __decorateClass([
|
|
949
|
+
common.Injectable()
|
|
950
|
+
], exports.SpotCatalogService);
|
|
951
|
+
exports.SpotPricesService = class SpotPricesService {
|
|
952
|
+
logger = new common.Logger(exports.SpotPricesService.name);
|
|
953
|
+
/** Les venues dont XGate sert les prix comptant. */
|
|
954
|
+
venues() {
|
|
955
|
+
return SPOT_XEXES;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Les prix comptant d'une ou plusieurs venues, en une seule liste.
|
|
959
|
+
*
|
|
960
|
+
* Même politique d'échec que partout : seule, une venue en échec fait échouer l'appel ; parmi
|
|
961
|
+
* d'autres, elle est journalisée et ignorée. Chaque prix porte son `xex`.
|
|
962
|
+
*/
|
|
963
|
+
async prices(...xexes) {
|
|
964
|
+
if (xexes.length === 0) {
|
|
965
|
+
throw new Error("spot prices() : aucune venue demand\xE9e \u2014 pr\xE9cise au moins un XgateEx.");
|
|
966
|
+
}
|
|
967
|
+
const solo = xexes.length === 1;
|
|
968
|
+
const collected = await Promise.all(
|
|
969
|
+
xexes.map(async (xex) => {
|
|
970
|
+
try {
|
|
971
|
+
return await this.pricesOf(xex);
|
|
972
|
+
} catch (error) {
|
|
973
|
+
if (solo === true) {
|
|
974
|
+
throw error;
|
|
975
|
+
}
|
|
976
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
977
|
+
this.logger.error(`spot prices(${xex}) a \xE9chou\xE9, venue ignor\xE9e : ${message}`);
|
|
978
|
+
return [];
|
|
979
|
+
}
|
|
980
|
+
})
|
|
981
|
+
);
|
|
982
|
+
return collected.flat();
|
|
983
|
+
}
|
|
984
|
+
/** Les prix comptant d'UNE venue, traduits vers {@link IPrice}. */
|
|
985
|
+
async pricesOf(xex) {
|
|
986
|
+
if (servesSpot(xex) === false) {
|
|
987
|
+
throw new Error(
|
|
988
|
+
`spot prices(${xex}) : cette venue ne sert pas de comptant. Disponibles : ${SPOT_XEXES.join(", ")}.`
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
const spot = createPublicSpotXex(xex).spot();
|
|
992
|
+
if (spot.getPrices === void 0) {
|
|
993
|
+
throw new Error(`spot prices(${xex}) : cette venue ne sert pas de prix comptant.`);
|
|
994
|
+
}
|
|
995
|
+
const prices = await spot.getPrices();
|
|
996
|
+
return prices.map((price) => ({
|
|
997
|
+
// `canonicalFromXex`, et non `canonicalFromBase` : les prix ne portent QUE le symbole
|
|
998
|
+
// concaténé (`BTCUSDT`), jamais la base — contrairement au catalogue, où la venue déclare
|
|
999
|
+
// `baseAsset`. Passer le symbole à `canonicalFromBase` rendrait `BTCUSDT` tel quel, puisque
|
|
1000
|
+
// cette fonction ne strippe volontairement aucune cotation.
|
|
1001
|
+
//
|
|
1002
|
+
// ⚠️ La limite est connue et assumée : le strip ne retire que `USDT`/`USDC` finaux, donc une
|
|
1003
|
+
// paire cotée en BTC (`ROSEBTC`, `ETHBTC`) garde son nom entier. Le catalogue comptant, lui,
|
|
1004
|
+
// résout ces cas — c'est là qu'il faut aller chercher `symbol` et `quote` justes.
|
|
1005
|
+
symbol: canonicalFromXex(price.name, xex),
|
|
1006
|
+
symbolXex: price.name,
|
|
1007
|
+
xex,
|
|
1008
|
+
kind: price.kind,
|
|
1009
|
+
mark: price.mark,
|
|
1010
|
+
oracle: price.oracle,
|
|
1011
|
+
mid: price.mid,
|
|
1012
|
+
bid: price.bid,
|
|
1013
|
+
ask: price.ask,
|
|
1014
|
+
last: price.last,
|
|
1015
|
+
funding: price.funding,
|
|
1016
|
+
openInterest: price.openInterest,
|
|
1017
|
+
volume24h: price.volume24h,
|
|
1018
|
+
prevDayPrice: price.prevDayPrice,
|
|
1019
|
+
quotedAt: price.quotedAt,
|
|
1020
|
+
xtras: price.xtras
|
|
1021
|
+
}));
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
exports.SpotPricesService = __decorateClass([
|
|
799
1025
|
common.Injectable()
|
|
800
|
-
], exports.
|
|
1026
|
+
], exports.SpotPricesService);
|
|
1027
|
+
exports.SpotWsCandlesService = class SpotWsCandlesService {
|
|
1028
|
+
logger = new common.Logger(exports.SpotWsCandlesService.name);
|
|
1029
|
+
/** Les venues dont XGate diffuse le comptant en temps réel. */
|
|
1030
|
+
venues() {
|
|
1031
|
+
return SPOT_XEXES;
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Souscrit aux bougies **comptant** d'un marché. Rend la fonction de désabonnement.
|
|
1035
|
+
*
|
|
1036
|
+
* Le handler reçoit une bougie à la fois, au format unifié ; la bougie en cours est repoussée à
|
|
1037
|
+
* chaque mise à jour tant qu'elle n'est pas close.
|
|
1038
|
+
*/
|
|
1039
|
+
subscribe(xex, query, handler) {
|
|
1040
|
+
if (servesSpot(xex) === false) {
|
|
1041
|
+
throw new Error(
|
|
1042
|
+
`spot subscribe(${xex}) : cette venue ne sert pas de comptant. Disponibles : ${SPOT_XEXES.join(", ")}.`
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
this.logger.log(`souscription bougies COMPTANT ${query.symbol} ${query.interval} sur ${xex}`);
|
|
1046
|
+
return createPublicSpotXex(xex).ws("spot").subscribeCandles({ name: query.symbol, interval: query.interval }, (wire) => {
|
|
1047
|
+
handler(toCandle(wire, xex, query.interval, ""));
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Souscrit au MÊME marché comptant chez plusieurs venues, avec un seul handler.
|
|
1052
|
+
*
|
|
1053
|
+
* Chaque bougie porte son `xex`. La fonction rendue coupe tous les flux d'un coup.
|
|
1054
|
+
*/
|
|
1055
|
+
subscribeAll(xexes, query, handler) {
|
|
1056
|
+
if (xexes.length === 0) {
|
|
1057
|
+
throw new Error("spot subscribeAll() : aucune venue demand\xE9e \u2014 pr\xE9cise au moins un XgateEx.");
|
|
1058
|
+
}
|
|
1059
|
+
const stops = [];
|
|
1060
|
+
for (const xex of xexes) {
|
|
1061
|
+
try {
|
|
1062
|
+
stops.push(this.subscribe(xex, query, handler));
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1065
|
+
this.logger.error(`souscription comptant ${xex} refus\xE9e, venue ignor\xE9e : ${message}`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return () => {
|
|
1069
|
+
for (const stop of stops) {
|
|
1070
|
+
stop();
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
exports.SpotWsCandlesService = __decorateClass([
|
|
1076
|
+
common.Injectable()
|
|
1077
|
+
], exports.SpotWsCandlesService);
|
|
801
1078
|
|
|
802
1079
|
// src/helpers/protection.ts
|
|
803
1080
|
var SLIPPAGE_DEFAUT = 5e-3;
|
|
@@ -893,7 +1170,8 @@ var ORDER_STATUSES = [
|
|
|
893
1170
|
];
|
|
894
1171
|
|
|
895
1172
|
// src/services/trading.service.ts
|
|
896
|
-
var VENUES_PROUVEES = ["pacifica" /* Pacifica */, "hyperliquid" /* Hyperliquid */];
|
|
1173
|
+
var VENUES_PROUVEES = ["pacifica" /* Pacifica */, "hyperliquid" /* Hyperliquid */, "aster" /* Aster */];
|
|
1174
|
+
var ASTER_TIMEOUT = "The request has timed out.";
|
|
897
1175
|
var VENUES_MOVE_STOP = ["hyperliquid" /* Hyperliquid */, "pacifica" /* Pacifica */, "aster" /* Aster */];
|
|
898
1176
|
exports.TradingService = class TradingService {
|
|
899
1177
|
logger = new common.Logger(exports.TradingService.name);
|
|
@@ -930,26 +1208,76 @@ exports.TradingService = class TradingService {
|
|
|
930
1208
|
`openWithProtection(${access.xex}) : cette venue n'expose pas l'ouverture prot\xE9g\xE9e atomique.`
|
|
931
1209
|
);
|
|
932
1210
|
}
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1211
|
+
const entry = {
|
|
1212
|
+
name: input.symbolXex,
|
|
1213
|
+
side,
|
|
1214
|
+
type: "limit",
|
|
1215
|
+
size: String(input.size),
|
|
1216
|
+
price: String(input.entry),
|
|
1217
|
+
tif: input.tif ?? "ioc",
|
|
1218
|
+
reduceOnly: false,
|
|
1219
|
+
clientId: input.clientId
|
|
1220
|
+
};
|
|
1221
|
+
const consigne = {
|
|
1222
|
+
name: input.symbolXex,
|
|
1223
|
+
side,
|
|
1224
|
+
sl: protection.sl,
|
|
1225
|
+
tps: protection.tps,
|
|
1226
|
+
clientId: input.clientId
|
|
1227
|
+
};
|
|
1228
|
+
try {
|
|
1229
|
+
const orders = await perp.createEntryWithProtection(entry, consigne);
|
|
1230
|
+
return orders.map((order) => this.toOrder(order, access.xex));
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1233
|
+
if (message.includes(ASTER_TIMEOUT) === false) {
|
|
1234
|
+
throw error;
|
|
950
1235
|
}
|
|
1236
|
+
this.logger.warn(
|
|
1237
|
+
`openWithProtection(${access.xex}) : la venue a coup\xE9 avant de r\xE9pondre. Statut inconnu \u2014 relecture de l'\xE9tat r\xE9el.`
|
|
1238
|
+
);
|
|
1239
|
+
return await this.etatApres(access, input.symbolXex);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* CE QUE LA VENUE A RÉELLEMENT FAIT, après une réponse perdue.
|
|
1244
|
+
*
|
|
1245
|
+
* Un timeout ne dit pas « rien n'a été créé » : mesuré sur aster, deux exécutions identiques ont
|
|
1246
|
+
* produit deux sous-ensembles différents — une fois le stop et un take-profit, une fois l'entrée
|
|
1247
|
+
* seule. La seule vérité est au carnet, donc on va l'y chercher.
|
|
1248
|
+
*
|
|
1249
|
+
* **Lève si la position est nue** : une entrée remplie sans stop est exactement ce que ce service
|
|
1250
|
+
* existe pour empêcher, et le silence serait pire que l'erreur. L'appelant doit savoir qu'il a une
|
|
1251
|
+
* position à protéger, tout de suite.
|
|
1252
|
+
*/
|
|
1253
|
+
async etatApres(access, symbolXex) {
|
|
1254
|
+
const perp = this.perpOf(access);
|
|
1255
|
+
const [ouverts, positions] = await Promise.all([
|
|
1256
|
+
perp.getOpens(),
|
|
1257
|
+
perp.getPositions({ name: symbolXex })
|
|
1258
|
+
]);
|
|
1259
|
+
const surLaPaire = ouverts.filter((order) => order.name === symbolXex);
|
|
1260
|
+
const position = positions.find(
|
|
1261
|
+
(candidate) => candidate.name === symbolXex && Number(candidate.size) !== 0
|
|
951
1262
|
);
|
|
952
|
-
|
|
1263
|
+
const protege = surLaPaire.some((order) => order.reduceOnly === true);
|
|
1264
|
+
if (position !== void 0 && protege === false) {
|
|
1265
|
+
throw new Error(
|
|
1266
|
+
`openWithProtection(${access.xex}/${symbolXex}) : POSITION NUE \u2014 ${position.size} ouvert sans stop au carnet apr\xE8s une r\xE9ponse perdue. \xC0 prot\xE9ger imm\xE9diatement.`
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
if (position === void 0 && surLaPaire.length > 0) {
|
|
1270
|
+
for (const orphelin of surLaPaire) {
|
|
1271
|
+
await perp.cancel({ name: symbolXex, id: orphelin.id }).catch(() => void 0);
|
|
1272
|
+
}
|
|
1273
|
+
throw new Error(
|
|
1274
|
+
`openWithProtection(${access.xex}/${symbolXex}) : r\xE9ponse perdue, lot incomplet \u2014 ${surLaPaire.length} ordre(s) orphelin(s) ANNUL\xC9(S). Rien n'est ouvert, rien ne tra\xEEne.`
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
this.logger.log(
|
|
1278
|
+
`openWithProtection(${access.xex}/${symbolXex}) : ${surLaPaire.length} ordre(s) au carnet, position ${position === void 0 ? "absente" : position.size}.`
|
|
1279
|
+
);
|
|
1280
|
+
return surLaPaire.map((order) => this.toOrder(order, access.xex));
|
|
953
1281
|
}
|
|
954
1282
|
/** Les positions ouvertes d'un compte. */
|
|
955
1283
|
async positions(access, symbolXex) {
|
|
@@ -1091,6 +1419,193 @@ exports.TradingService = class TradingService {
|
|
|
1091
1419
|
const last = exits[0];
|
|
1092
1420
|
return last === void 0 ? null : last.price;
|
|
1093
1421
|
}
|
|
1422
|
+
/**
|
|
1423
|
+
* L'HISTORIQUE DES ORDRES du compte — ce qui a été soumis, rempli, annulé ou expiré.
|
|
1424
|
+
*
|
|
1425
|
+
* À distinguer de {@link trades} : un ordre est une **intention**, une exécution est un **fait**.
|
|
1426
|
+
* Un ordre `filled` peut avoir été rempli en plusieurs fois, à des prix différents.
|
|
1427
|
+
*
|
|
1428
|
+
* Servi par les huit venues qui tradent.
|
|
1429
|
+
*/
|
|
1430
|
+
async orderHistory(access, symbolXex) {
|
|
1431
|
+
const orders = await this.perpOf(access).getHistory(
|
|
1432
|
+
symbolXex === void 0 ? void 0 : { name: symbolXex }
|
|
1433
|
+
);
|
|
1434
|
+
return orders.map((order) => this.toOrder(order, access.xex));
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* L'ÉTAT DU COMPTE — équité, disponible, marge immobilisée, marge de maintenance, PnL latent.
|
|
1438
|
+
*
|
|
1439
|
+
* Les venues rangent ces valeurs sous des noms qui n'ont rien à voir : `marginSummary.accountValue`
|
|
1440
|
+
* chez hyperliquid, `accountEquity` chez pacifica, `totalMarginBalance` chez aster — **mais elles
|
|
1441
|
+
* disent la même chose**. La forme native complète reste dans `xtras`, rien n'est perdu.
|
|
1442
|
+
*
|
|
1443
|
+
* **blofin ne l'expose pas** et lève ; les sept autres le servent.
|
|
1444
|
+
*/
|
|
1445
|
+
async accountInfo(access) {
|
|
1446
|
+
const perp = this.perpOf(access);
|
|
1447
|
+
if (typeof perp.getAccountInfo !== "function") {
|
|
1448
|
+
throw new Error(
|
|
1449
|
+
`accountInfo(${access.xex}) : cette venue n'expose pas l'\xE9tat de son compte.`
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
const brut = await perp.getAccountInfo();
|
|
1453
|
+
return this.toAccountState(brut, access.xex);
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* RÈGLE LE LEVIER d'une paire, avant d'ouvrir.
|
|
1457
|
+
*
|
|
1458
|
+
* Le levier détermine la marge immobilisée, donc la taille tenable : le changer après l'ouverture
|
|
1459
|
+
* ne rejoue pas la position déjà prise. C'est un réglage préalable, pas un ajustement.
|
|
1460
|
+
*
|
|
1461
|
+
* **bullet ne l'expose pas** — son levier se fixe au compte, pas à la position — et lève.
|
|
1462
|
+
*/
|
|
1463
|
+
async setLeverage(access, symbolXex, leverage) {
|
|
1464
|
+
const perp = this.perpOf(access);
|
|
1465
|
+
if (typeof perp.updateLeverage !== "function") {
|
|
1466
|
+
throw new Error(`setLeverage(${access.xex}) : cette venue ne r\xE8gle pas le levier par paire.`);
|
|
1467
|
+
}
|
|
1468
|
+
await perp.updateLeverage({ name: symbolXex, leverage });
|
|
1469
|
+
this.logger.log(`setLeverage(${access.xex}/${symbolXex}) : levier port\xE9 \xE0 ${leverage}`);
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* CHOISIT LE MODE DE MARGE d'une paire : **isolée** ou **croisée**.
|
|
1473
|
+
*
|
|
1474
|
+
* En **croisée**, tout le solde du compte garantit la position : elle tient plus longtemps, mais
|
|
1475
|
+
* une liquidation emporte l'ensemble. En **isolée**, seule la marge affectée est en jeu : la perte
|
|
1476
|
+
* est bornée à ce montant, la liquidation arrive plus tôt.
|
|
1477
|
+
*
|
|
1478
|
+
* **À régler AVANT d'ouvrir.** Changer le mode d'une position vivante est refusé par la plupart
|
|
1479
|
+
* des venues, et là où c'est accepté, cela redistribue la garantie sous les pieds de la position.
|
|
1480
|
+
*
|
|
1481
|
+
* Servi par les trois venues de référence.
|
|
1482
|
+
*/
|
|
1483
|
+
async setMarginMode(access, symbolXex, isolated) {
|
|
1484
|
+
const perp = this.perpOf(access);
|
|
1485
|
+
if (typeof perp.setMarginMode !== "function") {
|
|
1486
|
+
throw new Error(
|
|
1487
|
+
`setMarginMode(${access.xex}) : cette venue ne choisit pas son mode de marge.`
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
await perp.setMarginMode({ name: symbolXex, isolated });
|
|
1491
|
+
this.logger.log(
|
|
1492
|
+
`setMarginMode(${access.xex}/${symbolXex}) : marge ${isolated === true ? "ISOL\xC9E" : "CROIS\xC9E"}`
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* AJOUTE DE LA MARGE à une position isolée — pour éloigner le prix de liquidation.
|
|
1497
|
+
*
|
|
1498
|
+
* C'est le geste qui sauve une position sous pression sans la réduire : on renforce la garantie
|
|
1499
|
+
* plutôt que de couper. **N'a de sens qu'en marge isolée** ; en croisée, tout le solde garantit
|
|
1500
|
+
* déjà la position et il n'y a rien à ajouter.
|
|
1501
|
+
*
|
|
1502
|
+
* Servi par les trois venues de référence.
|
|
1503
|
+
*/
|
|
1504
|
+
async addMargin(access, symbolXex, amount) {
|
|
1505
|
+
const perp = this.perpOf(access);
|
|
1506
|
+
if (typeof perp.addIsolatedMargin !== "function") {
|
|
1507
|
+
throw new Error(`addMargin(${access.xex}) : cette venue n'ajoute pas de marge isol\xE9e.`);
|
|
1508
|
+
}
|
|
1509
|
+
await perp.addIsolatedMargin({ name: symbolXex, amount });
|
|
1510
|
+
this.logger.log(`addMargin(${access.xex}/${symbolXex}) : +${amount} de marge isol\xE9e`);
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* RETIRE DE LA MARGE d'une position isolée — pour libérer du capital.
|
|
1514
|
+
*
|
|
1515
|
+
* L'inverse d'{@link addMargin} : la garantie diminue, donc le prix de liquidation **se rapproche**.
|
|
1516
|
+
* La venue refuse ce qui mettrait la position sous son seuil de maintenance.
|
|
1517
|
+
*
|
|
1518
|
+
* ⚠️ **pacifica ne l'expose pas** et lève : chez elle, la marge s'ajoute mais ne se retire pas.
|
|
1519
|
+
* Fermer partiellement la position libère alors le capital.
|
|
1520
|
+
*/
|
|
1521
|
+
async removeMargin(access, symbolXex, amount) {
|
|
1522
|
+
const perp = this.perpOf(access);
|
|
1523
|
+
if (typeof perp.removeIsolatedMargin !== "function") {
|
|
1524
|
+
throw new Error(
|
|
1525
|
+
`removeMargin(${access.xex}) : cette venue ne retire pas de marge isol\xE9e \u2014 ferme partiellement la position pour lib\xE9rer du capital.`
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
await perp.removeIsolatedMargin({ name: symbolXex, amount });
|
|
1529
|
+
this.logger.log(`removeMargin(${access.xex}/${symbolXex}) : \u2212${amount} de marge isol\xE9e`);
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* L'HISTORIQUE DU FUNDING **PAYÉ** sur une paire — ce qui a réellement été prélevé ou reçu.
|
|
1533
|
+
*
|
|
1534
|
+
* À ne pas confondre avec le taux courant, que rend `PricesService` : celui-ci annonce ce qui
|
|
1535
|
+
* *sera* payé, celui-là dit ce qui *l'a été*. Un backtest qui ignore le funding réel surestime le
|
|
1536
|
+
* résultat d'une position tenue longtemps.
|
|
1537
|
+
*
|
|
1538
|
+
* Servi par les huit venues qui tradent.
|
|
1539
|
+
*/
|
|
1540
|
+
async fundingHistory(access, symbolXex, range) {
|
|
1541
|
+
const perp = this.perpOf(access);
|
|
1542
|
+
if (typeof perp.getFundingHistory !== "function") {
|
|
1543
|
+
throw new Error(
|
|
1544
|
+
`fundingHistory(${access.xex}) : cette venue ne publie pas son historique de funding.`
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
const rates = await perp.getFundingHistory({
|
|
1548
|
+
name: symbolXex,
|
|
1549
|
+
startTime: range?.startTime === void 0 ? void 0 : toXexDate(range.startTime),
|
|
1550
|
+
endTime: range?.endTime === void 0 ? void 0 : toXexDate(range.endTime),
|
|
1551
|
+
limit: range?.limit
|
|
1552
|
+
});
|
|
1553
|
+
return rates.map((rate) => ({
|
|
1554
|
+
xex: access.xex,
|
|
1555
|
+
symbolXex: rate.name,
|
|
1556
|
+
rate: rate.rate,
|
|
1557
|
+
appliedAt: rate.appliedAt,
|
|
1558
|
+
xtras: rate.xtras
|
|
1559
|
+
}));
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* UN IDENTIFIANT APPLICATIF pour un ordre, **au format que la venue accepte**.
|
|
1563
|
+
*
|
|
1564
|
+
* C'est lui qui relie un ordre à la décision qui l'a produit : la venue le rend tel quel dans
|
|
1565
|
+
* `IOrder.clientId`, ce qui permet de retrouver son origine sans tenir de table de correspondance.
|
|
1566
|
+
*
|
|
1567
|
+
* **hyperliquid exige un hexadécimal préfixé `0x`** (128 bits) là où les autres acceptent un UUID
|
|
1568
|
+
* ordinaire. Passer le mauvais format fait rejeter l'ordre — d'où cette méthode plutôt qu'un
|
|
1569
|
+
* `randomUUID()` chez l'appelant.
|
|
1570
|
+
*/
|
|
1571
|
+
newClientOrderId(xex) {
|
|
1572
|
+
const uuid = crypto.randomUUID();
|
|
1573
|
+
return xex === "hyperliquid" /* Hyperliquid */ ? `0x${uuid.replace(/-/gu, "")}` : uuid;
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* LE COUPE-CIRCUIT : annule TOUS les ordres d'une paire, d'un geste.
|
|
1577
|
+
*
|
|
1578
|
+
* À réserver aux situations où l'on veut reprendre la main sans discuter — un état incohérent, un
|
|
1579
|
+
* arrêt d'urgence, une reprise après incident. Annuler un par un laisse une fenêtre pendant
|
|
1580
|
+
* laquelle certains ordres vivent encore.
|
|
1581
|
+
*
|
|
1582
|
+
* ⚠️ **Cela retire aussi les PROTECTIONS.** Stops et take-profits sont des ordres comme les
|
|
1583
|
+
* autres : une position ouverte se retrouve **nue** après ce geste. À n'employer que si la
|
|
1584
|
+
* position est fermée, ou si l'on repose une protection immédiatement — jamais pour « faire le
|
|
1585
|
+
* ménage » sur une position vivante.
|
|
1586
|
+
*
|
|
1587
|
+
* Rend le nombre d'ordres annulés quand la venue le publie, `null` sinon.
|
|
1588
|
+
*/
|
|
1589
|
+
async cancelAll(access, symbolXex) {
|
|
1590
|
+
const perp = this.perpOf(access);
|
|
1591
|
+
if (typeof perp.cancelAll !== "function") {
|
|
1592
|
+
throw new Error(`cancelAll(${access.xex}) : cette venue n'annule pas en masse.`);
|
|
1593
|
+
}
|
|
1594
|
+
const positions = await perp.getPositions({ name: symbolXex });
|
|
1595
|
+
const ouverte = positions.find(
|
|
1596
|
+
(candidate) => candidate.name === symbolXex && Number(candidate.size) !== 0
|
|
1597
|
+
);
|
|
1598
|
+
if (ouverte !== void 0) {
|
|
1599
|
+
this.logger.warn(
|
|
1600
|
+
`cancelAll(${access.xex}/${symbolXex}) : ${ouverte.size} EN POSITION \u2014 ses protections partent avec. La position sera NUE.`
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
const { cancelled } = await perp.cancelAll({ name: symbolXex });
|
|
1604
|
+
this.logger.log(
|
|
1605
|
+
`cancelAll(${access.xex}/${symbolXex}) : ${cancelled ?? "?"} ordre(s) annul\xE9(s)`
|
|
1606
|
+
);
|
|
1607
|
+
return cancelled;
|
|
1608
|
+
}
|
|
1094
1609
|
/** Annule un ordre par son identifiant de venue. */
|
|
1095
1610
|
async cancel(access, symbolXex, orderId) {
|
|
1096
1611
|
await this.perpOf(access).cancel({ name: symbolXex, id: orderId });
|
|
@@ -1100,6 +1615,27 @@ exports.TradingService = class TradingService {
|
|
|
1100
1615
|
const source = createAccountXex(access.xex, access);
|
|
1101
1616
|
return source.perp();
|
|
1102
1617
|
}
|
|
1618
|
+
/**
|
|
1619
|
+
* L'état natif d'une venue → {@link IAccountState}.
|
|
1620
|
+
*
|
|
1621
|
+
* Une lecture EXPLICITE par venue plutôt qu'un parcours à l'aveugle : les noms ne se devinent pas,
|
|
1622
|
+
* et prendre le premier champ qui ressemble à une équité produirait un chiffre faux sans que rien
|
|
1623
|
+
* ne le signale. `null` là où la venue ne publie pas la valeur — pas `0`, qui se lirait comme
|
|
1624
|
+
* « rien » alors que personne n'a rien dit.
|
|
1625
|
+
*/
|
|
1626
|
+
toAccountState(brut, xex) {
|
|
1627
|
+
const texte = (valeur) => valeur === void 0 || valeur === null ? null : String(valeur);
|
|
1628
|
+
const marge = brut.marginSummary;
|
|
1629
|
+
return {
|
|
1630
|
+
xex,
|
|
1631
|
+
equity: texte(marge?.accountValue) ?? texte(brut.accountEquity) ?? texte(brut.totalMarginBalance) ?? "0",
|
|
1632
|
+
available: texte(brut.withdrawable) ?? texte(brut.availableToSpend) ?? texte(brut.availableBalance),
|
|
1633
|
+
marginUsed: texte(marge?.totalMarginUsed) ?? texte(brut.totalMarginUsed) ?? texte(brut.totalInitialMargin),
|
|
1634
|
+
maintenanceMargin: texte(brut.crossMaintenanceMarginUsed) ?? texte(brut.crossMmr) ?? texte(brut.totalMaintMargin),
|
|
1635
|
+
unrealizedPnl: texte(brut.totalUnrealizedProfit),
|
|
1636
|
+
xtras: brut
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1103
1639
|
/**
|
|
1104
1640
|
* Le statut de la venue, VALIDÉ — jamais casté. Une venue qui inventerait un statut rendrait sinon
|
|
1105
1641
|
* une valeur absente de tous les `switch` en aval, et personne ne la rattraperait.
|
|
@@ -1130,6 +1666,9 @@ exports.TradingService = class TradingService {
|
|
|
1130
1666
|
exports.TradingService = __decorateClass([
|
|
1131
1667
|
common.Injectable()
|
|
1132
1668
|
], exports.TradingService);
|
|
1669
|
+
function toXexDate(date) {
|
|
1670
|
+
return date.toISOString().slice(0, 19).replace("T", " ");
|
|
1671
|
+
}
|
|
1133
1672
|
exports.WalletService = class WalletService {
|
|
1134
1673
|
logger = new common.Logger(exports.WalletService.name);
|
|
1135
1674
|
/** Les venues dont le portefeuille est lisible aujourd'hui. */
|
|
@@ -1333,6 +1872,176 @@ exports.WsCandlesService = class WsCandlesService {
|
|
|
1333
1872
|
exports.WsCandlesService = __decorateClass([
|
|
1334
1873
|
common.Injectable()
|
|
1335
1874
|
], exports.WsCandlesService);
|
|
1875
|
+
exports.WsTradesService = class WsTradesService {
|
|
1876
|
+
logger = new common.Logger(exports.WsTradesService.name);
|
|
1877
|
+
/**
|
|
1878
|
+
* S'abonne aux exécutions d'un compte. Rend la fonction de désabonnement.
|
|
1879
|
+
*
|
|
1880
|
+
* Le handler reçoit **une exécution à la fois**, au format unifié, et **uniquement celles
|
|
1881
|
+
* survenues après l'abonnement** — le rejeu d'historique de la venue est écarté.
|
|
1882
|
+
*/
|
|
1883
|
+
subscribe(access, handler) {
|
|
1884
|
+
const source = createAccountXex(access.xex, access);
|
|
1885
|
+
if (typeof source.ws !== "function") {
|
|
1886
|
+
throw new Error(`userTrades(${access.xex}) : cette venue n'expose pas de temps r\xE9el.`);
|
|
1887
|
+
}
|
|
1888
|
+
const ws = source.ws();
|
|
1889
|
+
if (typeof ws.subscribeUserTrades !== "function") {
|
|
1890
|
+
throw new Error(
|
|
1891
|
+
`userTrades(${access.xex}) : cette venue ne diffuse pas les ex\xE9cutions du compte.`
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
const depuis = Date.now();
|
|
1895
|
+
let ecartees = 0;
|
|
1896
|
+
this.logger.log(`souscription ex\xE9cutions sur ${access.xex}`);
|
|
1897
|
+
return ws.subscribeUserTrades((trade) => {
|
|
1898
|
+
if (trade.filledAt.getTime() < depuis) {
|
|
1899
|
+
ecartees += 1;
|
|
1900
|
+
if (ecartees === 1) {
|
|
1901
|
+
this.logger.log(`${access.xex} : rejeu d'historique \xE9cart\xE9 (ant\xE9rieur \xE0 l'abonnement).`);
|
|
1902
|
+
}
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
handler(this.toTrade(trade, access.xex));
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* S'abonne aux exécutions de PLUSIEURS comptes, avec un seul handler.
|
|
1910
|
+
*
|
|
1911
|
+
* Chaque exécution porte son `xex` : l'appelant sait toujours de quelle venue elle vient. La
|
|
1912
|
+
* fonction rendue coupe tous les flux d'un coup — sinon il faudrait en garder N et n'en oublier
|
|
1913
|
+
* aucun.
|
|
1914
|
+
*
|
|
1915
|
+
* Une venue qui refuse est journalisée et ignorée : le reste des comptes doit continuer d'être
|
|
1916
|
+
* suivi. Sur un seul accès, l'échec est celui de l'appel.
|
|
1917
|
+
*/
|
|
1918
|
+
subscribeAll(accesses, handler) {
|
|
1919
|
+
if (accesses.length === 0) {
|
|
1920
|
+
throw new Error("userTrades() : aucun compte demand\xE9 \u2014 pr\xE9cise au moins un acc\xE8s.");
|
|
1921
|
+
}
|
|
1922
|
+
const solo = accesses.length === 1;
|
|
1923
|
+
const stops = [];
|
|
1924
|
+
for (const access of accesses) {
|
|
1925
|
+
try {
|
|
1926
|
+
stops.push(this.subscribe(access, handler));
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
if (solo === true) {
|
|
1929
|
+
throw error;
|
|
1930
|
+
}
|
|
1931
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1932
|
+
this.logger.error(`souscription ${access.xex} refus\xE9e, venue ignor\xE9e : ${message}`);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
return () => {
|
|
1936
|
+
for (const stop of stops) {
|
|
1937
|
+
try {
|
|
1938
|
+
stop();
|
|
1939
|
+
} catch {
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* S'abonne aux ORDRES du compte — leur naissance, leur remplissage, leur mort.
|
|
1946
|
+
*
|
|
1947
|
+
* À distinguer des exécutions : un ordre est une **intention** dont on suit le cycle de vie
|
|
1948
|
+
* (`open` → `partiallyFilled` → `filled`, ou `canceled`), une exécution est un **fait** ponctuel.
|
|
1949
|
+
* Pour savoir qu'un stop vient de se déclencher, c'est ici qu'il faut écouter ; pour savoir à quel
|
|
1950
|
+
* prix, c'est {@link subscribe}.
|
|
1951
|
+
*
|
|
1952
|
+
* **Même règle que pour les exécutions** : seuls les ordres postérieurs à l'abonnement sont
|
|
1953
|
+
* transmis, le rejeu d'historique est écarté.
|
|
1954
|
+
*/
|
|
1955
|
+
subscribeOrders(access, handler) {
|
|
1956
|
+
const ws = this.wsOf(access);
|
|
1957
|
+
if (typeof ws.subscribeOrders !== "function") {
|
|
1958
|
+
throw new Error(`orders(${access.xex}) : cette venue ne diffuse pas ses ordres.`);
|
|
1959
|
+
}
|
|
1960
|
+
const depuis = Date.now();
|
|
1961
|
+
this.logger.log(`souscription ordres sur ${access.xex}`);
|
|
1962
|
+
return ws.subscribeOrders((order) => {
|
|
1963
|
+
if (order.placedAt.getTime() >= depuis) {
|
|
1964
|
+
handler(this.toOrder(order, access.xex));
|
|
1965
|
+
}
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* S'abonne aux TRANSACTIONS PUBLIQUES d'un marché — celles de tout le monde, pas les siennes.
|
|
1970
|
+
*
|
|
1971
|
+
* C'est le flux qui dit ce qui se négocie réellement : prix, taille, sens agresseur. Il sert à
|
|
1972
|
+
* mesurer l'activité d'une paire, pas à suivre son compte — pour cela, {@link subscribe}.
|
|
1973
|
+
*
|
|
1974
|
+
* **Aucun filtre temporel ici** : un trade public est daté de son exécution et arrive en direct,
|
|
1975
|
+
* il n'y a pas d'historique rejoué à écarter.
|
|
1976
|
+
*/
|
|
1977
|
+
subscribePublicTrades(access, symbolXex, handler) {
|
|
1978
|
+
const ws = this.wsOf(access);
|
|
1979
|
+
if (typeof ws.subscribeTrades !== "function") {
|
|
1980
|
+
throw new Error(`publicTrades(${access.xex}) : cette venue ne diffuse pas les transactions.`);
|
|
1981
|
+
}
|
|
1982
|
+
this.logger.log(`souscription transactions ${symbolXex} sur ${access.xex}`);
|
|
1983
|
+
return ws.subscribeTrades({ name: symbolXex }, (trade) => {
|
|
1984
|
+
handler({
|
|
1985
|
+
xex: access.xex,
|
|
1986
|
+
symbolXex: trade.name,
|
|
1987
|
+
price: trade.price,
|
|
1988
|
+
size: trade.size,
|
|
1989
|
+
side: trade.side,
|
|
1990
|
+
tradedAt: trade.tradedAt,
|
|
1991
|
+
xtras: trade.xtras
|
|
1992
|
+
});
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
/** Le client temps réel d'un compte, avec ses souscriptions signées. */
|
|
1996
|
+
wsOf(access) {
|
|
1997
|
+
const source = createAccountXex(access.xex, access);
|
|
1998
|
+
if (typeof source.ws !== "function") {
|
|
1999
|
+
throw new Error(`${access.xex} : cette venue n'expose pas de temps r\xE9el.`);
|
|
2000
|
+
}
|
|
2001
|
+
return source.ws();
|
|
2002
|
+
}
|
|
2003
|
+
toOrder(order, xex) {
|
|
2004
|
+
return {
|
|
2005
|
+
xex,
|
|
2006
|
+
symbolXex: order.name,
|
|
2007
|
+
kind: order.kind,
|
|
2008
|
+
id: order.id,
|
|
2009
|
+
clientId: order.clientId,
|
|
2010
|
+
side: order.side,
|
|
2011
|
+
type: order.type,
|
|
2012
|
+
price: order.price,
|
|
2013
|
+
size: order.size,
|
|
2014
|
+
filled: order.filled,
|
|
2015
|
+
status: ORDER_STATUSES.includes(order.status) ? order.status : "other",
|
|
2016
|
+
tif: order.tif,
|
|
2017
|
+
reduceOnly: order.reduceOnly,
|
|
2018
|
+
placedAt: order.placedAt,
|
|
2019
|
+
xtras: order.xtras
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
toTrade(trade, xex) {
|
|
2023
|
+
return {
|
|
2024
|
+
xex,
|
|
2025
|
+
symbolXex: trade.name,
|
|
2026
|
+
kind: trade.kind,
|
|
2027
|
+
id: trade.id,
|
|
2028
|
+
orderId: trade.orderId,
|
|
2029
|
+
side: trade.side,
|
|
2030
|
+
price: trade.price,
|
|
2031
|
+
size: trade.size,
|
|
2032
|
+
fee: trade.fee,
|
|
2033
|
+
feeAsset: trade.feeAsset,
|
|
2034
|
+
grossPnl: trade.grossPnl,
|
|
2035
|
+
netPnl: trade.netPnl,
|
|
2036
|
+
maker: trade.maker,
|
|
2037
|
+
filledAt: trade.filledAt,
|
|
2038
|
+
xtras: trade.xtras
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
};
|
|
2042
|
+
exports.WsTradesService = __decorateClass([
|
|
2043
|
+
common.Injectable()
|
|
2044
|
+
], exports.WsTradesService);
|
|
1336
2045
|
|
|
1337
2046
|
// src/xgate.module.ts
|
|
1338
2047
|
exports.XgateModule = class XgateModule {
|
|
@@ -1342,29 +2051,39 @@ exports.XgateModule = __decorateClass([
|
|
|
1342
2051
|
providers: [
|
|
1343
2052
|
exports.CatalogService,
|
|
1344
2053
|
exports.CandlesService,
|
|
2054
|
+
exports.CandlesStreamRegistry,
|
|
1345
2055
|
exports.WsCandlesService,
|
|
2056
|
+
exports.WsTradesService,
|
|
1346
2057
|
exports.PricesService,
|
|
1347
2058
|
exports.SpotCatalogService,
|
|
1348
2059
|
exports.SpotCandlesService,
|
|
2060
|
+
exports.SpotPricesService,
|
|
2061
|
+
exports.SpotWsCandlesService,
|
|
1349
2062
|
exports.WalletService
|
|
1350
2063
|
],
|
|
1351
2064
|
exports: [
|
|
1352
2065
|
exports.CatalogService,
|
|
1353
2066
|
exports.CandlesService,
|
|
2067
|
+
exports.CandlesStreamRegistry,
|
|
1354
2068
|
exports.WsCandlesService,
|
|
2069
|
+
exports.WsTradesService,
|
|
1355
2070
|
exports.PricesService,
|
|
1356
2071
|
exports.SpotCatalogService,
|
|
1357
2072
|
exports.SpotCandlesService,
|
|
2073
|
+
exports.SpotPricesService,
|
|
2074
|
+
exports.SpotWsCandlesService,
|
|
1358
2075
|
exports.WalletService
|
|
1359
2076
|
]
|
|
1360
2077
|
})
|
|
1361
2078
|
], exports.XgateModule);
|
|
1362
2079
|
|
|
2080
|
+
exports.CandlesStreamService = CandlesStreamService;
|
|
1363
2081
|
exports.ORDER_STATUSES = ORDER_STATUSES;
|
|
1364
2082
|
exports.SPOT_XEXES = SPOT_XEXES;
|
|
1365
2083
|
exports.TIMEFRAME_MINUTES = TIMEFRAME_MINUTES;
|
|
1366
2084
|
exports.Timeframe = Timeframe;
|
|
1367
2085
|
exports.WALLET_XEXES = WALLET_XEXES;
|
|
2086
|
+
exports.WS_SUBSCRIPTION_LIMITS = WS_SUBSCRIPTION_LIMITS;
|
|
1368
2087
|
exports.XGATE_EXCHANGES = XGATE_EXCHANGES;
|
|
1369
2088
|
exports.XgateEx = XgateEx;
|
|
1370
2089
|
exports.buildLevels = buildLevels;
|
|
@@ -1379,6 +2098,7 @@ exports.roundToStep = roundToStep;
|
|
|
1379
2098
|
exports.servesNatively = servesNatively;
|
|
1380
2099
|
exports.servesSpot = servesSpot;
|
|
1381
2100
|
exports.sourceFor = sourceFor;
|
|
2101
|
+
exports.subscriptionLimitOf = subscriptionLimitOf;
|
|
1382
2102
|
exports.timeframeMs = timeframeMs;
|
|
1383
2103
|
//# sourceMappingURL=index.cjs.map
|
|
1384
2104
|
//# sourceMappingURL=index.cjs.map
|