@blackcube/xgate-sdk 0.20.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/LICENSE +28 -0
- package/README.md +142 -0
- package/dist/index.cjs +1165 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1048 -0
- package/dist/index.d.ts +1048 -0
- package/dist/index.js +1147 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1147 @@
|
|
|
1
|
+
import { Injectable, Module, Logger } from '@nestjs/common';
|
|
2
|
+
import { Aster } from '@blackcube/aster-sdk';
|
|
3
|
+
import { Binance } from '@blackcube/binance-sdk';
|
|
4
|
+
import { Blofin } from '@blackcube/blofin-sdk';
|
|
5
|
+
import { Bullet } from '@blackcube/bullet-sdk';
|
|
6
|
+
import { Bybit } from '@blackcube/bybit-sdk';
|
|
7
|
+
import { Extended } from '@blackcube/extended-sdk';
|
|
8
|
+
import { Hyperliquid } from '@blackcube/hyperliquid-sdk';
|
|
9
|
+
import { Lighter } from '@blackcube/lighter-sdk';
|
|
10
|
+
import { Pacifica } from '@blackcube/pacifica-sdk';
|
|
11
|
+
import { Paradex } from '@blackcube/paradex-sdk';
|
|
12
|
+
|
|
13
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
14
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
15
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
16
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
17
|
+
if (decorator = decorators[i])
|
|
18
|
+
result = (decorator(result)) || result;
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/enums/timeframe.enums.ts
|
|
23
|
+
var Timeframe = /* @__PURE__ */ ((Timeframe2) => {
|
|
24
|
+
Timeframe2["M1"] = "1m";
|
|
25
|
+
Timeframe2["M5"] = "5m";
|
|
26
|
+
Timeframe2["M15"] = "15m";
|
|
27
|
+
Timeframe2["H1"] = "1h";
|
|
28
|
+
Timeframe2["H4"] = "4h";
|
|
29
|
+
Timeframe2["D"] = "1d";
|
|
30
|
+
Timeframe2["W"] = "1w";
|
|
31
|
+
return Timeframe2;
|
|
32
|
+
})(Timeframe || {});
|
|
33
|
+
var TIMEFRAME_MINUTES = {
|
|
34
|
+
["1m" /* M1 */]: 1,
|
|
35
|
+
["5m" /* M5 */]: 5,
|
|
36
|
+
["15m" /* M15 */]: 15,
|
|
37
|
+
["1h" /* H1 */]: 60,
|
|
38
|
+
["4h" /* H4 */]: 240,
|
|
39
|
+
["1d" /* D */]: 1440,
|
|
40
|
+
["1w" /* W */]: 10080
|
|
41
|
+
};
|
|
42
|
+
function timeframeMs(timeframe) {
|
|
43
|
+
return TIMEFRAME_MINUTES[timeframe] * 6e4;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/helpers/decimal.ts
|
|
47
|
+
function parse(value) {
|
|
48
|
+
const trimmed = value.trim();
|
|
49
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed) === false) {
|
|
50
|
+
throw new Error(`D\xE9cimal illisible : "${value}".`);
|
|
51
|
+
}
|
|
52
|
+
const dot = trimmed.indexOf(".");
|
|
53
|
+
if (dot < 0) {
|
|
54
|
+
return { digits: BigInt(trimmed), scale: 0 };
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
digits: BigInt(trimmed.slice(0, dot) + trimmed.slice(dot + 1)),
|
|
58
|
+
scale: trimmed.length - dot - 1
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function format(digits, scale) {
|
|
62
|
+
const negative = digits < 0n;
|
|
63
|
+
const plain = (negative === true ? -digits : digits).toString().padStart(scale + 1, "0");
|
|
64
|
+
const sign = negative === true ? "-" : "";
|
|
65
|
+
if (scale === 0) {
|
|
66
|
+
return sign + plain;
|
|
67
|
+
}
|
|
68
|
+
const whole = plain.slice(0, plain.length - scale);
|
|
69
|
+
const fraction = plain.slice(plain.length - scale).replace(/0+$/, "");
|
|
70
|
+
return fraction === "" ? sign + whole : `${sign + whole}.${fraction}`;
|
|
71
|
+
}
|
|
72
|
+
function addDecimals(left, right) {
|
|
73
|
+
const a = parse(left);
|
|
74
|
+
const b = parse(right);
|
|
75
|
+
const scale = Math.max(a.scale, b.scale);
|
|
76
|
+
const digits = a.digits * 10n ** BigInt(scale - a.scale) + b.digits * 10n ** BigInt(scale - b.scale);
|
|
77
|
+
return format(digits, scale);
|
|
78
|
+
}
|
|
79
|
+
function compareDecimals(left, right) {
|
|
80
|
+
const a = parse(left);
|
|
81
|
+
const b = parse(right);
|
|
82
|
+
const scale = Math.max(a.scale, b.scale);
|
|
83
|
+
const x = a.digits * 10n ** BigInt(scale - a.scale);
|
|
84
|
+
const y = b.digits * 10n ** BigInt(scale - b.scale);
|
|
85
|
+
if (x === y) {
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
return x > y ? 1 : -1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/helpers/candle-aggregator.ts
|
|
92
|
+
var MONDAY_OFFSET_MS = 4 * 24 * 60 * 60 * 1e3;
|
|
93
|
+
function aggregate(source, target, now = Date.now()) {
|
|
94
|
+
if (source.length === 0) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
const sourceMs = timeframeMs(source[0]?.interval ?? "1m" /* M1 */);
|
|
98
|
+
const targetMs = timeframeMs(target);
|
|
99
|
+
if (targetMs <= sourceMs) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
const offsetMs = target === "1w" /* W */ ? MONDAY_OFFSET_MS : 0;
|
|
103
|
+
const expected = targetMs / sourceMs;
|
|
104
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
105
|
+
const sorted = [...source].sort(
|
|
106
|
+
(left, right) => left.openedAt.getTime() - right.openedAt.getTime()
|
|
107
|
+
);
|
|
108
|
+
for (const candle of sorted) {
|
|
109
|
+
const start = Math.floor((candle.openedAt.getTime() - offsetMs) / targetMs) * targetMs + offsetMs;
|
|
110
|
+
const bucket = buckets.get(start);
|
|
111
|
+
if (bucket === void 0) {
|
|
112
|
+
buckets.set(start, {
|
|
113
|
+
sources: 1,
|
|
114
|
+
candle: {
|
|
115
|
+
...candle,
|
|
116
|
+
interval: target,
|
|
117
|
+
openedAt: new Date(start),
|
|
118
|
+
// Dernière milliseconde du créneau, comme les bougies servies par les venues.
|
|
119
|
+
closedAt: new Date(start + targetMs - 1),
|
|
120
|
+
derived: true
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
bucket.sources += 1;
|
|
125
|
+
const built = bucket.candle;
|
|
126
|
+
built.high = compareDecimals(candle.high, built.high) > 0 ? candle.high : built.high;
|
|
127
|
+
built.low = compareDecimals(candle.low, built.low) < 0 ? candle.low : built.low;
|
|
128
|
+
built.close = candle.close;
|
|
129
|
+
built.volume = sum(built.volume, candle.volume);
|
|
130
|
+
built.quoteVolume = sum(built.quoteVolume, candle.quoteVolume);
|
|
131
|
+
built.takerBuyQuoteVolume = sum(built.takerBuyQuoteVolume, candle.takerBuyQuoteVolume);
|
|
132
|
+
built.trades = built.trades === null || candle.trades === null ? null : built.trades + candle.trades;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return [...buckets.keys()].sort((left, right) => left - right).map((start) => buckets.get(start)).filter((bucket) => bucket !== void 0).filter((bucket) => {
|
|
136
|
+
const stillOpen = bucket.candle.openedAt.getTime() + targetMs > now;
|
|
137
|
+
return bucket.sources >= expected || stillOpen === true;
|
|
138
|
+
}).map((bucket) => bucket.candle);
|
|
139
|
+
}
|
|
140
|
+
function sum(left, right) {
|
|
141
|
+
if (left === null || right === null) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return addDecimals(left, right);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/helpers/symbol-naming.ts
|
|
148
|
+
var CONCAT_QUOTE_XEXS = /* @__PURE__ */ new Set(["aster", "binance", "bybit"]);
|
|
149
|
+
var K_MULTIPLIER = /^k(?=[A-Z])/u;
|
|
150
|
+
var NUMERIC_MULTIPLIER = /^10{3,}(?=[A-Z0-9])/u;
|
|
151
|
+
var STOCK_24_5 = /_24_5$/u;
|
|
152
|
+
var TICKER_ALIASES = {
|
|
153
|
+
GOOG: "GOOGL",
|
|
154
|
+
// classe d'action (extended `GOOG` ≡ `GOOGL` ailleurs)
|
|
155
|
+
// Indices
|
|
156
|
+
US500: "SP500",
|
|
157
|
+
SPX500M: "SP500",
|
|
158
|
+
TECH100M: "US100",
|
|
159
|
+
// Matières premières
|
|
160
|
+
BRENTOIL: "BRENT",
|
|
161
|
+
NATGAS: "NG",
|
|
162
|
+
// Actions (USD collé sur lighter, non strippé → aliasé explicitement)
|
|
163
|
+
SAMSUNGUSD: "SAMSUNG",
|
|
164
|
+
SKHYNIXUSD: "SKHYNIX",
|
|
165
|
+
HYUNDAIUSD: "HYUNDAI"
|
|
166
|
+
};
|
|
167
|
+
function canonicalFromXex(rawSymbol, xexId) {
|
|
168
|
+
let s = rawSymbol.replace(K_MULTIPLIER, "").toUpperCase();
|
|
169
|
+
if (CONCAT_QUOTE_XEXS.has(xexId)) {
|
|
170
|
+
s = s.replace(/(USDT|USDC)$/u, "");
|
|
171
|
+
} else {
|
|
172
|
+
s = s.replace(/-USD-PERP$/u, "").replace(/-PERP$/u, "").replace(/-USDT$/u, "").replace(/-USDC$/u, "").replace(/-USD$/u, "");
|
|
173
|
+
}
|
|
174
|
+
s = s.replace(NUMERIC_MULTIPLIER, "");
|
|
175
|
+
s = s.replace(STOCK_24_5, "");
|
|
176
|
+
return TICKER_ALIASES[s] ?? s;
|
|
177
|
+
}
|
|
178
|
+
function canonicalFromBase(base) {
|
|
179
|
+
const s = base.replace(K_MULTIPLIER, "").toUpperCase().replace(NUMERIC_MULTIPLIER, "").replace(STOCK_24_5, "");
|
|
180
|
+
return TICKER_ALIASES[s] ?? s;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/helpers/candle-mapper.ts
|
|
184
|
+
function toCandle(wire, xex, interval, quote) {
|
|
185
|
+
return {
|
|
186
|
+
symbol: canonicalFromXex(wire.symbol, xex),
|
|
187
|
+
quote,
|
|
188
|
+
symbolXex: wire.symbol,
|
|
189
|
+
xex,
|
|
190
|
+
kind: wire.kind,
|
|
191
|
+
interval,
|
|
192
|
+
openedAt: wire.openedAt,
|
|
193
|
+
closedAt: wire.closedAt,
|
|
194
|
+
open: wire.open,
|
|
195
|
+
high: wire.high,
|
|
196
|
+
low: wire.low,
|
|
197
|
+
close: wire.close,
|
|
198
|
+
volume: wire.volume,
|
|
199
|
+
quoteVolume: wire.quoteVolume,
|
|
200
|
+
takerBuyQuoteVolume: wire.takerBuyQuoteVolume,
|
|
201
|
+
trades: wire.trades > 0 ? wire.trades : null,
|
|
202
|
+
// Servie par la venue, pas fabriquée. L'agrégation posera `true`.
|
|
203
|
+
derived: false
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/helpers/candle-query.ts
|
|
208
|
+
function toXexQuery(query) {
|
|
209
|
+
return {
|
|
210
|
+
name: query.symbol,
|
|
211
|
+
interval: query.interval,
|
|
212
|
+
startTime: query.startTime,
|
|
213
|
+
endTime: query.endTime,
|
|
214
|
+
limit: query.limit
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/enums/xgate-ex.enums.ts
|
|
219
|
+
var XgateEx = /* @__PURE__ */ ((XgateEx2) => {
|
|
220
|
+
XgateEx2["Hyperliquid"] = "hyperliquid";
|
|
221
|
+
XgateEx2["Pacifica"] = "pacifica";
|
|
222
|
+
XgateEx2["Aster"] = "aster";
|
|
223
|
+
XgateEx2["Lighter"] = "lighter";
|
|
224
|
+
XgateEx2["Extended"] = "extended";
|
|
225
|
+
XgateEx2["Paradex"] = "paradex";
|
|
226
|
+
XgateEx2["Bullet"] = "bullet";
|
|
227
|
+
XgateEx2["Blofin"] = "blofin";
|
|
228
|
+
XgateEx2["Binance"] = "binance";
|
|
229
|
+
XgateEx2["Bybit"] = "bybit";
|
|
230
|
+
return XgateEx2;
|
|
231
|
+
})(XgateEx || {});
|
|
232
|
+
var XGATE_EXCHANGES = Object.values(XgateEx);
|
|
233
|
+
|
|
234
|
+
// src/helpers/xex-registry.ts
|
|
235
|
+
function createAccountXex(xex, access) {
|
|
236
|
+
const NULL_KEY = `0x${"0".repeat(64)}`;
|
|
237
|
+
const adresse = () => {
|
|
238
|
+
if (access.address === void 0 || access.address === "") {
|
|
239
|
+
throw new Error(`wallet(${xex}) : il faut au moins une adresse (\`access.address\`).`);
|
|
240
|
+
}
|
|
241
|
+
return access.address;
|
|
242
|
+
};
|
|
243
|
+
switch (xex) {
|
|
244
|
+
case "hyperliquid" /* Hyperliquid */:
|
|
245
|
+
return new Hyperliquid(
|
|
246
|
+
{
|
|
247
|
+
a: {
|
|
248
|
+
privateKey: access.privateKey ?? NULL_KEY,
|
|
249
|
+
publicKey: adresse(),
|
|
250
|
+
network: access.network ?? "mainnet"
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
{ default: "a" }
|
|
254
|
+
);
|
|
255
|
+
case "pacifica" /* Pacifica */:
|
|
256
|
+
return new Pacifica(
|
|
257
|
+
{
|
|
258
|
+
a: {
|
|
259
|
+
secretKey: access.privateKey ?? "",
|
|
260
|
+
publicKey: adresse(),
|
|
261
|
+
network: access.network ?? "mainnet"
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
{ default: "a" }
|
|
265
|
+
);
|
|
266
|
+
case "aster" /* Aster */:
|
|
267
|
+
if (access.privateKey === void 0 || access.privateKey === "") {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`wallet(${xex}) : Aster signe avec une cl\xE9 d'API \u2014 il faut \`privateKey\` (la cl\xE9) et \`address\` (le compte EVM).`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
return new Aster(
|
|
273
|
+
{
|
|
274
|
+
a: {
|
|
275
|
+
privateKey: access.privateKey,
|
|
276
|
+
user: adresse(),
|
|
277
|
+
network: access.network ?? "mainnet"
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
{ default: "a" }
|
|
281
|
+
);
|
|
282
|
+
case "blofin" /* Blofin */:
|
|
283
|
+
if (access.apiKey === void 0 || access.secret === void 0) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`wallet(${xex}) : BloFin signe m\xEAme en lecture \u2014 il faut \`apiKey\`, \`secret\` et \`passphrase\`.`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
return new Blofin({
|
|
289
|
+
credentials: {
|
|
290
|
+
apiKey: access.apiKey,
|
|
291
|
+
secret: access.secret,
|
|
292
|
+
passphrase: access.passphrase ?? ""
|
|
293
|
+
},
|
|
294
|
+
network: access.network ?? "mainnet"
|
|
295
|
+
});
|
|
296
|
+
default:
|
|
297
|
+
throw new Error(
|
|
298
|
+
`wallet(${xex}) : venue pas encore c\xE2bl\xE9e c\xF4t\xE9 compte. Disponibles : ${WALLET_XEXES.join(", ")}.`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
var WALLET_XEXES = [
|
|
303
|
+
"hyperliquid" /* Hyperliquid */,
|
|
304
|
+
"pacifica" /* Pacifica */,
|
|
305
|
+
"aster" /* Aster */,
|
|
306
|
+
"blofin" /* Blofin */
|
|
307
|
+
];
|
|
308
|
+
function createPublicXex(xex) {
|
|
309
|
+
switch (xex) {
|
|
310
|
+
case "hyperliquid" /* Hyperliquid */:
|
|
311
|
+
return new Hyperliquid();
|
|
312
|
+
case "pacifica" /* Pacifica */:
|
|
313
|
+
return new Pacifica();
|
|
314
|
+
case "aster" /* Aster */:
|
|
315
|
+
return new Aster();
|
|
316
|
+
case "lighter" /* Lighter */:
|
|
317
|
+
return new Lighter();
|
|
318
|
+
case "extended" /* Extended */:
|
|
319
|
+
return new Extended();
|
|
320
|
+
case "paradex" /* Paradex */:
|
|
321
|
+
return new Paradex();
|
|
322
|
+
case "bullet" /* Bullet */:
|
|
323
|
+
return new Bullet();
|
|
324
|
+
case "blofin" /* Blofin */:
|
|
325
|
+
return new Blofin();
|
|
326
|
+
case "binance" /* Binance */:
|
|
327
|
+
return new Binance();
|
|
328
|
+
case "bybit" /* Bybit */:
|
|
329
|
+
return new Bybit();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
var SPOT_XEXES = ["binance" /* Binance */, "bybit" /* Bybit */];
|
|
333
|
+
function servesSpot(xex) {
|
|
334
|
+
return SPOT_XEXES.includes(xex);
|
|
335
|
+
}
|
|
336
|
+
function createPublicSpotXex(xex) {
|
|
337
|
+
switch (xex) {
|
|
338
|
+
case "binance" /* Binance */:
|
|
339
|
+
return new Binance();
|
|
340
|
+
case "bybit" /* Bybit */:
|
|
341
|
+
return new Bybit();
|
|
342
|
+
default:
|
|
343
|
+
throw new Error(
|
|
344
|
+
`spot(${xex}) : cette venue ne sert pas de comptant via XGate. Venues avec spot : ${SPOT_XEXES.join(", ")}.`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/helpers/xex-timeframes.ts
|
|
350
|
+
var NATIVE = {
|
|
351
|
+
["hyperliquid" /* Hyperliquid */]: Object.values(Timeframe),
|
|
352
|
+
["pacifica" /* Pacifica */]: Object.values(Timeframe),
|
|
353
|
+
["aster" /* Aster */]: Object.values(Timeframe),
|
|
354
|
+
["blofin" /* Blofin */]: Object.values(Timeframe),
|
|
355
|
+
["lighter" /* Lighter */]: [
|
|
356
|
+
"1m" /* M1 */,
|
|
357
|
+
"5m" /* M5 */,
|
|
358
|
+
"15m" /* M15 */,
|
|
359
|
+
"1h" /* H1 */,
|
|
360
|
+
"4h" /* H4 */,
|
|
361
|
+
"1d" /* D */
|
|
362
|
+
],
|
|
363
|
+
["extended" /* Extended */]: [
|
|
364
|
+
"1m" /* M1 */,
|
|
365
|
+
"5m" /* M5 */,
|
|
366
|
+
"15m" /* M15 */,
|
|
367
|
+
"1h" /* H1 */,
|
|
368
|
+
"4h" /* H4 */,
|
|
369
|
+
"1d" /* D */
|
|
370
|
+
],
|
|
371
|
+
["paradex" /* Paradex */]: ["1m" /* M1 */, "5m" /* M5 */, "15m" /* M15 */, "1h" /* H1 */],
|
|
372
|
+
["bullet" /* Bullet */]: [],
|
|
373
|
+
// Les deux CEX servent les sept, et sous les mêmes noms côté binance. Bybit compte en minutes
|
|
374
|
+
// (`60`, `240`) puis en lettres (`D`, `W`) : son SDK traduit, la table reste dans notre grille.
|
|
375
|
+
["binance" /* Binance */]: Object.values(Timeframe),
|
|
376
|
+
["bybit" /* Bybit */]: Object.values(Timeframe)
|
|
377
|
+
};
|
|
378
|
+
function servesNatively(xex, interval) {
|
|
379
|
+
return NATIVE[xex].includes(interval);
|
|
380
|
+
}
|
|
381
|
+
function sourceFor(xex, target) {
|
|
382
|
+
const targetMinutes = TIMEFRAME_MINUTES[target];
|
|
383
|
+
const candidates = NATIVE[xex].filter((interval) => {
|
|
384
|
+
const minutes = TIMEFRAME_MINUTES[interval];
|
|
385
|
+
return minutes < targetMinutes && targetMinutes % minutes === 0;
|
|
386
|
+
}).sort((left, right) => TIMEFRAME_MINUTES[right] - TIMEFRAME_MINUTES[left]);
|
|
387
|
+
return candidates[0];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/services/candles.service.ts
|
|
391
|
+
var CandlesService = class {
|
|
392
|
+
logger = new Logger(CandlesService.name);
|
|
393
|
+
/**
|
|
394
|
+
* Les bougies d'une venue, sur une plage de **dates**.
|
|
395
|
+
*
|
|
396
|
+
* L'appelant donne des `Date` et un {@link Timeframe} ; le service traduit vers ce que la venue
|
|
397
|
+
* attend — chaîne UTC pour huit d'entre elles, millisecondes pour blofin.
|
|
398
|
+
*
|
|
399
|
+
* **Un intervalle que la venue ne sert pas est FABRIQUÉ**, pas refusé : on tire l'intervalle
|
|
400
|
+
* exploitable le plus grand et on agrège. Paradex s'arrête à l'heure, lighter et extended ne
|
|
401
|
+
* font pas la semaine — l'appelant n'a pas à le savoir. Les bougies fabriquées portent
|
|
402
|
+
* `derived: true`.
|
|
403
|
+
*
|
|
404
|
+
* **Lève si la venue ne sert rien du tout** (bullet, qui n'a pas de REST). Rendre une liste vide
|
|
405
|
+
* ferait passer une incapacité pour une absence de données, et un backfill silencieusement troué
|
|
406
|
+
* est pire qu'un backfill en erreur.
|
|
407
|
+
*/
|
|
408
|
+
async candles(xex, query) {
|
|
409
|
+
if (servesNatively(xex, query.interval) === true) {
|
|
410
|
+
return this.fetch(xex, query, query.interval);
|
|
411
|
+
}
|
|
412
|
+
const source = sourceFor(xex, query.interval);
|
|
413
|
+
if (source === void 0) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
`candles(${xex}) : ni ${query.interval} servi, ni intervalle plus fin pour le fabriquer.`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
this.logger.log(
|
|
419
|
+
`${xex} ne sert pas ${query.interval} : agr\xE9gation depuis ${source} (bougies d\xE9riv\xE9es).`
|
|
420
|
+
);
|
|
421
|
+
const fine = await this.fetch(xex, query, source);
|
|
422
|
+
return aggregate(fine, query.interval);
|
|
423
|
+
}
|
|
424
|
+
/** Un appel REST à la venue, dans l'intervalle qu'elle sait servir. */
|
|
425
|
+
async fetch(xex, query, interval) {
|
|
426
|
+
const perp = createPublicXex(xex).perp();
|
|
427
|
+
if (perp.getCandles === void 0) {
|
|
428
|
+
throw new Error(
|
|
429
|
+
`candles(${xex}) : cette venue ne sert pas de bougies en REST \u2014 utilise WsCandlesService.`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
const wires = await perp.getCandles(toXexQuery({ ...query, interval }));
|
|
433
|
+
return wires.map((wire) => toCandle(wire, xex, interval, this.quoteOf(wire.symbol)));
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Les bougies d'un même marché chez PLUSIEURS venues, en une liste.
|
|
437
|
+
*
|
|
438
|
+
* Comme pour le catalogue : une venue en échec parmi d'autres est journalisée et ignorée ; seule,
|
|
439
|
+
* son échec est celui de l'appel. Chaque bougie porte son `xex`, donc la liste reste
|
|
440
|
+
* exploitable telle quelle.
|
|
441
|
+
*/
|
|
442
|
+
async candlesOf(xexes, query) {
|
|
443
|
+
if (xexes.length === 0) {
|
|
444
|
+
throw new Error("candlesOf() : aucune venue demand\xE9e \u2014 pr\xE9cise au moins un XgateEx.");
|
|
445
|
+
}
|
|
446
|
+
const solo = xexes.length === 1;
|
|
447
|
+
const collected = await Promise.all(
|
|
448
|
+
xexes.map(async (xex) => {
|
|
449
|
+
try {
|
|
450
|
+
return await this.candles(xex, query);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
if (solo === true) {
|
|
453
|
+
throw error;
|
|
454
|
+
}
|
|
455
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
456
|
+
this.logger.error(
|
|
457
|
+
`candles(${xex}, ${query.interval}) a \xE9chou\xE9, venue ignor\xE9e : ${message}`
|
|
458
|
+
);
|
|
459
|
+
return [];
|
|
460
|
+
}
|
|
461
|
+
})
|
|
462
|
+
);
|
|
463
|
+
return collected.flat();
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* La cotation, déduite du symbole natif. Les bougies ne la portent pas — seul le catalogue la
|
|
467
|
+
* publie —, et refaire un appel de catalogue pour chaque série coûterait plus cher que ce
|
|
468
|
+
* qu'elle apporte. `BTC-USDT` → `USDT` ; à défaut de séparateur, on ne prétend rien.
|
|
469
|
+
*/
|
|
470
|
+
quoteOf(symbolXex) {
|
|
471
|
+
const parts = symbolXex.split("-");
|
|
472
|
+
const last = parts[parts.length - 1];
|
|
473
|
+
return parts.length > 1 && last !== void 0 ? last : "";
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
CandlesService = __decorateClass([
|
|
477
|
+
Injectable()
|
|
478
|
+
], CandlesService);
|
|
479
|
+
var NAMES_ITS_BASE = /* @__PURE__ */ new Set(["binance" /* Binance */, "bybit" /* Bybit */]);
|
|
480
|
+
var CatalogService = class {
|
|
481
|
+
logger = new Logger(CatalogService.name);
|
|
482
|
+
/**
|
|
483
|
+
* Le catalogue **perp** d'une ou plusieurs venues, en une seule liste.
|
|
484
|
+
*
|
|
485
|
+
* Chaque ligne porte sa venue d'origine (`xex`) et les deux symboles — le canonique pour
|
|
486
|
+
* rapprocher un actif entre venues, le brut pour parler à celle-ci.
|
|
487
|
+
*
|
|
488
|
+
* **Le traitement d'une venue en échec dépend de ce qui a été demandé** : sur une seule venue,
|
|
489
|
+
* un échec est l'échec de l'appel et il est relancé ; sur plusieurs, la liste partielle garde
|
|
490
|
+
* sa valeur et l'incident est journalisé. Rendre `[]` en silence serait le pire des deux — un
|
|
491
|
+
* catalogue vide se lit comme « cette venue ne liste rien », pas comme « je n'ai pas pu
|
|
492
|
+
* demander ».
|
|
493
|
+
*/
|
|
494
|
+
async catalog(...xexes) {
|
|
495
|
+
if (xexes.length === 0) {
|
|
496
|
+
throw new Error("catalog() : aucune venue demand\xE9e \u2014 pr\xE9cise au moins un XgateEx.");
|
|
497
|
+
}
|
|
498
|
+
const solo = xexes.length === 1;
|
|
499
|
+
const collected = await Promise.all(
|
|
500
|
+
xexes.map(async (xex) => {
|
|
501
|
+
try {
|
|
502
|
+
return await this.catalogOf(xex);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
if (solo === true) {
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
508
|
+
this.logger.error(`catalog(${xex}) a \xE9chou\xE9, venue ignor\xE9e : ${message}`);
|
|
509
|
+
return [];
|
|
510
|
+
}
|
|
511
|
+
})
|
|
512
|
+
);
|
|
513
|
+
return collected.flat();
|
|
514
|
+
}
|
|
515
|
+
/** Le catalogue d'UNE venue, traduit vers {@link IPair}. */
|
|
516
|
+
async catalogOf(xex) {
|
|
517
|
+
const pairs = await createPublicXex(xex).perp().getPairs();
|
|
518
|
+
return pairs.map((pair) => ({
|
|
519
|
+
symbol: NAMES_ITS_BASE.has(xex) ? canonicalFromBase(pair.base) : canonicalFromXex(pair.name, xex),
|
|
520
|
+
quote: pair.quote,
|
|
521
|
+
symbolXex: pair.name,
|
|
522
|
+
xex,
|
|
523
|
+
kind: pair.kind,
|
|
524
|
+
ref: pair.ref ?? null,
|
|
525
|
+
szDecimals: pair.szDecimals,
|
|
526
|
+
tickSize: pair.tickSize,
|
|
527
|
+
stepSize: pair.stepSize,
|
|
528
|
+
minQty: pair.minQty,
|
|
529
|
+
minNotional: pair.minNotional,
|
|
530
|
+
maxLeverage: pair.maxLeverage,
|
|
531
|
+
status: pair.status,
|
|
532
|
+
delisted: pair.delisted,
|
|
533
|
+
listedAt: pair.listedAt,
|
|
534
|
+
xtras: pair.xtras
|
|
535
|
+
}));
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
CatalogService = __decorateClass([
|
|
539
|
+
Injectable()
|
|
540
|
+
], CatalogService);
|
|
541
|
+
var PricesService = class {
|
|
542
|
+
logger = new Logger(PricesService.name);
|
|
543
|
+
/**
|
|
544
|
+
* Les prix **perp** d'une ou plusieurs venues, en une seule liste.
|
|
545
|
+
*
|
|
546
|
+
* **Le traitement d'une venue en échec dépend de ce qui a été demandé** : sur une seule venue, un
|
|
547
|
+
* échec est l'échec de l'appel et il est relancé ; sur plusieurs, la liste partielle garde sa
|
|
548
|
+
* valeur et l'incident est journalisé. Rendre `[]` en silence serait le pire des deux — un relevé
|
|
549
|
+
* vide se lit comme « ce marché ne cote rien », pas comme « je n'ai pas pu demander ».
|
|
550
|
+
*/
|
|
551
|
+
async prices(...xexes) {
|
|
552
|
+
if (xexes.length === 0) {
|
|
553
|
+
throw new Error("prices() : aucune venue demand\xE9e \u2014 pr\xE9cise au moins un XgateEx.");
|
|
554
|
+
}
|
|
555
|
+
const solo = xexes.length === 1;
|
|
556
|
+
const collected = await Promise.all(
|
|
557
|
+
xexes.map(async (xex) => {
|
|
558
|
+
try {
|
|
559
|
+
return await this.pricesOf(xex);
|
|
560
|
+
} catch (error) {
|
|
561
|
+
if (solo === true) {
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
564
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
565
|
+
this.logger.error(`prices(${xex}) a \xE9chou\xE9, venue ignor\xE9e : ${message}`);
|
|
566
|
+
return [];
|
|
567
|
+
}
|
|
568
|
+
})
|
|
569
|
+
);
|
|
570
|
+
return collected.flat();
|
|
571
|
+
}
|
|
572
|
+
/** Les prix d'UNE venue, traduits vers {@link IPrice}. */
|
|
573
|
+
async pricesOf(xex) {
|
|
574
|
+
const prices = await createPublicXex(xex).perp().getPrices();
|
|
575
|
+
return prices.map((price) => ({
|
|
576
|
+
symbol: canonicalFromXex(price.name, xex),
|
|
577
|
+
symbolXex: price.name,
|
|
578
|
+
xex,
|
|
579
|
+
kind: price.kind,
|
|
580
|
+
mark: price.mark,
|
|
581
|
+
oracle: price.oracle,
|
|
582
|
+
mid: price.mid,
|
|
583
|
+
bid: price.bid,
|
|
584
|
+
ask: price.ask,
|
|
585
|
+
last: price.last,
|
|
586
|
+
funding: price.funding,
|
|
587
|
+
openInterest: price.openInterest,
|
|
588
|
+
volume24h: price.volume24h,
|
|
589
|
+
prevDayPrice: price.prevDayPrice,
|
|
590
|
+
quotedAt: price.quotedAt,
|
|
591
|
+
xtras: price.xtras
|
|
592
|
+
}));
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
PricesService = __decorateClass([
|
|
596
|
+
Injectable()
|
|
597
|
+
], PricesService);
|
|
598
|
+
var SpotCatalogService = class {
|
|
599
|
+
logger = new Logger(SpotCatalogService.name);
|
|
600
|
+
/** Les venues dont XGate sert le comptant. */
|
|
601
|
+
venues() {
|
|
602
|
+
return SPOT_XEXES;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Le catalogue **spot** d'une ou plusieurs venues, en une seule liste.
|
|
606
|
+
*
|
|
607
|
+
* Même politique d'échec que le catalogue perpétuel : sur une seule venue, l'échec est relancé ;
|
|
608
|
+
* sur plusieurs, la liste partielle garde sa valeur et l'incident est journalisé.
|
|
609
|
+
*
|
|
610
|
+
* **Une venue sans comptant lève**, elle ne rend pas une liste vide : « cette venue n'a pas de
|
|
611
|
+
* spot » et « cette venue ne cote rien au comptant » ne se corrigent pas de la même façon.
|
|
612
|
+
*/
|
|
613
|
+
async catalog(...xexes) {
|
|
614
|
+
if (xexes.length === 0) {
|
|
615
|
+
throw new Error("spot catalog() : aucune venue demand\xE9e \u2014 pr\xE9cise au moins un XgateEx.");
|
|
616
|
+
}
|
|
617
|
+
const solo = xexes.length === 1;
|
|
618
|
+
const collected = await Promise.all(
|
|
619
|
+
xexes.map(async (xex) => {
|
|
620
|
+
try {
|
|
621
|
+
return await this.catalogOf(xex);
|
|
622
|
+
} catch (error) {
|
|
623
|
+
if (solo === true) {
|
|
624
|
+
throw error;
|
|
625
|
+
}
|
|
626
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
627
|
+
this.logger.error(`spot catalog(${xex}) a \xE9chou\xE9, venue ignor\xE9e : ${message}`);
|
|
628
|
+
return [];
|
|
629
|
+
}
|
|
630
|
+
})
|
|
631
|
+
);
|
|
632
|
+
return collected.flat();
|
|
633
|
+
}
|
|
634
|
+
/** Le catalogue comptant d'UNE venue, traduit vers {@link IPair}. */
|
|
635
|
+
async catalogOf(xex) {
|
|
636
|
+
if (servesSpot(xex) === false) {
|
|
637
|
+
throw new Error(`spot catalog(${xex}) : cette venue ne sert pas de comptant via XGate.`);
|
|
638
|
+
}
|
|
639
|
+
const pairs = await createPublicSpotXex(xex).spot().getPairs();
|
|
640
|
+
return pairs.map((pair) => ({
|
|
641
|
+
// Les deux venues à comptant NOMMENT leur actif de base : on prend ce qu'elles disent. Re-parser
|
|
642
|
+
// `ROSEBTC` donnerait `ROSEBTC`, le strip de cotation ne connaissant que USDT et USDC — et le
|
|
643
|
+
// spot se cote massivement en BTC, ETH ou BNB.
|
|
644
|
+
symbol: canonicalFromBase(pair.base),
|
|
645
|
+
quote: pair.quote,
|
|
646
|
+
symbolXex: pair.name,
|
|
647
|
+
xex,
|
|
648
|
+
kind: pair.kind,
|
|
649
|
+
ref: pair.ref ?? null,
|
|
650
|
+
szDecimals: pair.szDecimals,
|
|
651
|
+
tickSize: pair.tickSize,
|
|
652
|
+
stepSize: pair.stepSize,
|
|
653
|
+
minQty: pair.minQty,
|
|
654
|
+
minNotional: pair.minNotional,
|
|
655
|
+
maxLeverage: pair.maxLeverage,
|
|
656
|
+
status: pair.status,
|
|
657
|
+
delisted: pair.delisted,
|
|
658
|
+
listedAt: pair.listedAt,
|
|
659
|
+
xtras: pair.xtras
|
|
660
|
+
}));
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
SpotCatalogService = __decorateClass([
|
|
664
|
+
Injectable()
|
|
665
|
+
], SpotCatalogService);
|
|
666
|
+
|
|
667
|
+
// src/helpers/protection.ts
|
|
668
|
+
var SLIPPAGE_DEFAUT = 5e-3;
|
|
669
|
+
var SEUIL_CLOTURE = 0.8;
|
|
670
|
+
function round8(value) {
|
|
671
|
+
return Number(value.toFixed(8));
|
|
672
|
+
}
|
|
673
|
+
function roundToStep(value, step, mode) {
|
|
674
|
+
if (step === null || step === void 0 || step === "") {
|
|
675
|
+
return Math.round(value * 1e4) / 1e4;
|
|
676
|
+
}
|
|
677
|
+
const stepNum = Number(step);
|
|
678
|
+
if (Number.isFinite(stepNum) === false || stepNum <= 0) {
|
|
679
|
+
return Math.round(value * 1e4) / 1e4;
|
|
680
|
+
}
|
|
681
|
+
const quotient = value / stepNum;
|
|
682
|
+
const units = mode === "floor" ? Math.floor(Number(quotient.toFixed(9))) : Math.round(quotient);
|
|
683
|
+
const dot = step.indexOf(".");
|
|
684
|
+
const decimals = dot === -1 ? 0 : step.length - dot - 1;
|
|
685
|
+
return Number((units * stepNum).toFixed(decimals));
|
|
686
|
+
}
|
|
687
|
+
function roundPrice(value, tickSize, lotSize) {
|
|
688
|
+
if (tickSize !== null && tickSize !== void 0 && tickSize !== "") {
|
|
689
|
+
return roundToStep(value, tickSize, "nearest");
|
|
690
|
+
}
|
|
691
|
+
const dot = (lotSize ?? "").indexOf(".");
|
|
692
|
+
const szDecimals = dot === -1 ? 0 : lotSize.length - dot - 1;
|
|
693
|
+
const maxDecimals = 6 - szDecimals;
|
|
694
|
+
const sig = Number(value.toPrecision(5));
|
|
695
|
+
if (maxDecimals <= 0) {
|
|
696
|
+
return Math.round(sig);
|
|
697
|
+
}
|
|
698
|
+
return Number(sig.toFixed(maxDecimals));
|
|
699
|
+
}
|
|
700
|
+
function buildLevels(entry, direction, slPct, tpPcts) {
|
|
701
|
+
const dir = direction === "long" ? 1 : -1;
|
|
702
|
+
return {
|
|
703
|
+
sl: round8(entry * (1 - dir * slPct)),
|
|
704
|
+
tps: tpPcts.map((pct) => round8(entry * (1 + dir * pct)))
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function buildProtection(input) {
|
|
708
|
+
const slippage = input.slippagePct ?? SLIPPAGE_DEFAUT;
|
|
709
|
+
const long = input.direction === "long";
|
|
710
|
+
const { sl, tps: niveaux } = buildLevels(
|
|
711
|
+
input.entry,
|
|
712
|
+
input.direction,
|
|
713
|
+
input.slPct,
|
|
714
|
+
input.tps.map((tp) => tp.pct)
|
|
715
|
+
);
|
|
716
|
+
const borne = (trigger) => long === true ? trigger * (1 - slippage) : trigger * (1 + slippage);
|
|
717
|
+
const sommeParts = input.tps.reduce((total, tp) => total + tp.part, 0);
|
|
718
|
+
const clotureIntegrale = sommeParts > SEUIL_CLOTURE;
|
|
719
|
+
const legs = [];
|
|
720
|
+
let alloue = 0;
|
|
721
|
+
niveaux.forEach((trigger, index) => {
|
|
722
|
+
const dernier = index === niveaux.length - 1;
|
|
723
|
+
const part = input.tps[index]?.part ?? 0;
|
|
724
|
+
if (dernier === false && part <= 0) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const size = dernier === true && clotureIntegrale === true ? roundToStep(input.size - alloue, input.lotSize, "floor") : roundToStep(part * input.size, input.lotSize, "floor");
|
|
728
|
+
if (size <= 0) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
alloue += size;
|
|
732
|
+
legs.push({
|
|
733
|
+
triggerPrice: String(roundPrice(trigger, input.tickSize, input.lotSize)),
|
|
734
|
+
size: String(size),
|
|
735
|
+
price: String(roundPrice(borne(trigger), input.tickSize, input.lotSize))
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
return {
|
|
739
|
+
sl: {
|
|
740
|
+
triggerPrice: String(roundPrice(sl, input.tickSize, input.lotSize)),
|
|
741
|
+
// Taille PLEINE, toujours : le stop couvre l'intégralité de la position.
|
|
742
|
+
size: String(input.size),
|
|
743
|
+
price: String(roundPrice(borne(sl), input.tickSize, input.lotSize))
|
|
744
|
+
},
|
|
745
|
+
tps: legs
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// src/interfaces/trading.interfaces.ts
|
|
750
|
+
var ORDER_STATUSES = [
|
|
751
|
+
"open",
|
|
752
|
+
"partiallyFilled",
|
|
753
|
+
"filled",
|
|
754
|
+
"canceled",
|
|
755
|
+
"rejected",
|
|
756
|
+
"expired",
|
|
757
|
+
"other"
|
|
758
|
+
];
|
|
759
|
+
|
|
760
|
+
// src/services/trading.service.ts
|
|
761
|
+
var VENUES_PROUVEES = ["pacifica" /* Pacifica */, "hyperliquid" /* Hyperliquid */];
|
|
762
|
+
var TradingService = class {
|
|
763
|
+
logger = new Logger(TradingService.name);
|
|
764
|
+
/**
|
|
765
|
+
* OUVRE UNE POSITION AVEC SA PROTECTION, en un geste atomique.
|
|
766
|
+
*
|
|
767
|
+
* Le premier ordre rendu est **l'entrée** ; les suivants sont le stop et les take-profits, dans
|
|
768
|
+
* l'ordre où ils ont été posés.
|
|
769
|
+
*
|
|
770
|
+
* Chaque venue applique son mécanisme natif — hyperliquid groupe les enfants sous l'entrée et les
|
|
771
|
+
* annule lui-même si elle rate, pacifica les embarque dans l'ordre, aster envoie un lot de
|
|
772
|
+
* conditionnels. Le SDK de la venue s'en charge ; xgate fournit des niveaux justes.
|
|
773
|
+
*/
|
|
774
|
+
async openWithProtection(access, input) {
|
|
775
|
+
if (VENUES_PROUVEES.includes(access.xex) === false) {
|
|
776
|
+
throw new Error(
|
|
777
|
+
`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.`
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
const protection = buildProtection({
|
|
781
|
+
entry: input.entry,
|
|
782
|
+
direction: input.direction,
|
|
783
|
+
size: input.size,
|
|
784
|
+
slPct: input.slPct,
|
|
785
|
+
tps: input.tps,
|
|
786
|
+
tickSize: input.tickSize,
|
|
787
|
+
lotSize: input.lotSize,
|
|
788
|
+
slippagePct: input.slippagePct
|
|
789
|
+
});
|
|
790
|
+
const side = input.direction === "long" ? "buy" : "sell";
|
|
791
|
+
const perp = this.perpOf(access);
|
|
792
|
+
if (typeof perp.createEntryWithProtection !== "function") {
|
|
793
|
+
throw new Error(
|
|
794
|
+
`openWithProtection(${access.xex}) : cette venue n'expose pas l'ouverture prot\xE9g\xE9e atomique.`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
const orders = await perp.createEntryWithProtection(
|
|
798
|
+
{
|
|
799
|
+
name: input.symbolXex,
|
|
800
|
+
side,
|
|
801
|
+
type: "limit",
|
|
802
|
+
size: String(input.size),
|
|
803
|
+
price: String(input.entry),
|
|
804
|
+
tif: input.tif ?? "ioc",
|
|
805
|
+
reduceOnly: false,
|
|
806
|
+
clientId: input.clientId
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
name: input.symbolXex,
|
|
810
|
+
side,
|
|
811
|
+
sl: protection.sl,
|
|
812
|
+
tps: protection.tps,
|
|
813
|
+
clientId: input.clientId
|
|
814
|
+
}
|
|
815
|
+
);
|
|
816
|
+
return orders.map((order) => this.toOrder(order, access.xex));
|
|
817
|
+
}
|
|
818
|
+
/** Les positions ouvertes d'un compte. */
|
|
819
|
+
async positions(access, symbolXex) {
|
|
820
|
+
const perp = this.perpOf(access);
|
|
821
|
+
const positions = await perp.getPositions(
|
|
822
|
+
symbolXex === void 0 ? void 0 : { name: symbolXex }
|
|
823
|
+
);
|
|
824
|
+
return positions.map((position) => ({
|
|
825
|
+
xex: access.xex,
|
|
826
|
+
symbolXex: position.name,
|
|
827
|
+
symbol: canonicalFromXex(position.name, access.xex),
|
|
828
|
+
side: position.side,
|
|
829
|
+
size: position.size,
|
|
830
|
+
entryPrice: position.entryPrice,
|
|
831
|
+
markPrice: position.markPrice,
|
|
832
|
+
unrealizedPnl: position.unrealizedPnl,
|
|
833
|
+
leverage: position.leverage,
|
|
834
|
+
liquidationPrice: position.liquidationPrice,
|
|
835
|
+
margin: position.margin,
|
|
836
|
+
xtras: position.xtras
|
|
837
|
+
}));
|
|
838
|
+
}
|
|
839
|
+
/** Les ordres encore au carnet. */
|
|
840
|
+
async openOrders(access) {
|
|
841
|
+
const orders = await this.perpOf(access).getOpens();
|
|
842
|
+
return orders.map((order) => this.toOrder(order, access.xex));
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Les exécutions du compte — ce qui a réellement été rempli.
|
|
846
|
+
*
|
|
847
|
+
* À ne pas confondre avec les ordres : un ordre annoncé `filled` peut avoir été rempli en
|
|
848
|
+
* plusieurs fois, à des prix différents. C'est ici que se lit le prix moyen réel.
|
|
849
|
+
*/
|
|
850
|
+
async trades(access) {
|
|
851
|
+
const trades = await this.perpOf(access).getUserTrades();
|
|
852
|
+
return trades.map((trade) => ({
|
|
853
|
+
xex: access.xex,
|
|
854
|
+
symbolXex: trade.name,
|
|
855
|
+
kind: trade.kind,
|
|
856
|
+
id: trade.id,
|
|
857
|
+
orderId: trade.orderId,
|
|
858
|
+
side: trade.side,
|
|
859
|
+
price: trade.price,
|
|
860
|
+
size: trade.size,
|
|
861
|
+
fee: trade.fee,
|
|
862
|
+
feeAsset: trade.feeAsset,
|
|
863
|
+
grossPnl: trade.grossPnl,
|
|
864
|
+
netPnl: trade.netPnl,
|
|
865
|
+
maker: trade.maker,
|
|
866
|
+
filledAt: trade.filledAt,
|
|
867
|
+
xtras: trade.xtras
|
|
868
|
+
}));
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* FERME UNE POSITION ENTIÈREMENT, sur la taille que **la venue** déclare — jamais celle qu'on croit.
|
|
872
|
+
*
|
|
873
|
+
* C'est toute la différence : fermer la taille planifiée laisse un reliquat quand le fill a différé du
|
|
874
|
+
* plan (une entrée partiellement remplie, un take-profit passé entre-temps), et ce reliquat reste
|
|
875
|
+
* ouvert, sans stop, à la charge du compte. On relit donc la position juste avant de la fermer.
|
|
876
|
+
*
|
|
877
|
+
* Rend `null` quand il n'y a rien à fermer — un appel sur une position déjà close n'est pas une erreur.
|
|
878
|
+
*/
|
|
879
|
+
async closePosition(access, symbolXex) {
|
|
880
|
+
const perp = this.perpOf(access);
|
|
881
|
+
const positions = await perp.getPositions({ name: symbolXex });
|
|
882
|
+
const position = positions.find(
|
|
883
|
+
(candidate) => candidate.name === symbolXex && Number(candidate.size) !== 0
|
|
884
|
+
);
|
|
885
|
+
let closed = null;
|
|
886
|
+
if (position !== void 0) {
|
|
887
|
+
const exit = position.side === "long" ? "sell" : "buy";
|
|
888
|
+
const order = await perp.place({
|
|
889
|
+
name: symbolXex,
|
|
890
|
+
side: exit,
|
|
891
|
+
type: "market",
|
|
892
|
+
size: position.size,
|
|
893
|
+
reduceOnly: true
|
|
894
|
+
});
|
|
895
|
+
closed = this.toOrder(order, access.xex);
|
|
896
|
+
this.logger.log(
|
|
897
|
+
`closePosition(${access.xex}/${symbolXex}) : ${position.size} ferm\xE9 au march\xE9`
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
return closed;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* LE PRIX DE SORTIE RÉEL d'une position, lu dans l'historique de la venue.
|
|
904
|
+
*
|
|
905
|
+
* Le prix planifié ne dit pas à quel prix on est sorti : un stop se déclenche au marché, un
|
|
906
|
+
* take-profit peut remplir en plusieurs fois. Seul l'historique porte le prix réellement obtenu, et
|
|
907
|
+
* c'est lui qui doit servir au calcul du résultat — sinon le PnL affiché est une fiction.
|
|
908
|
+
*
|
|
909
|
+
* On retient le **dernier** ordre de sortie rempli après `openedAt` : les précédents sont les
|
|
910
|
+
* take-profits partiels, celui-là est la clôture.
|
|
911
|
+
*/
|
|
912
|
+
async exitPrice(access, symbolXex, direction, openedAt) {
|
|
913
|
+
const exit = direction === "long" ? "sell" : "buy";
|
|
914
|
+
const history = await this.perpOf(access).getHistory({ name: symbolXex });
|
|
915
|
+
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());
|
|
916
|
+
const last = exits[0];
|
|
917
|
+
return last === void 0 ? null : last.price;
|
|
918
|
+
}
|
|
919
|
+
/** Annule un ordre par son identifiant de venue. */
|
|
920
|
+
async cancel(access, symbolXex, orderId) {
|
|
921
|
+
await this.perpOf(access).cancel({ name: symbolXex, id: orderId });
|
|
922
|
+
}
|
|
923
|
+
/** Le scope `perp()` de la venue, typé au plus près de ce que le contrat commun garantit. */
|
|
924
|
+
perpOf(access) {
|
|
925
|
+
const source = createAccountXex(access.xex, access);
|
|
926
|
+
return source.perp();
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Le statut de la venue, VALIDÉ — jamais casté. Une venue qui inventerait un statut rendrait sinon
|
|
930
|
+
* une valeur absente de tous les `switch` en aval, et personne ne la rattraperait.
|
|
931
|
+
*/
|
|
932
|
+
toStatus(status) {
|
|
933
|
+
return ORDER_STATUSES.includes(status) === true ? status : "other";
|
|
934
|
+
}
|
|
935
|
+
toOrder(order, xex) {
|
|
936
|
+
return {
|
|
937
|
+
xex,
|
|
938
|
+
symbolXex: order.name,
|
|
939
|
+
kind: order.kind,
|
|
940
|
+
id: order.id,
|
|
941
|
+
clientId: order.clientId,
|
|
942
|
+
side: order.side,
|
|
943
|
+
type: order.type,
|
|
944
|
+
price: order.price,
|
|
945
|
+
size: order.size,
|
|
946
|
+
filled: order.filled,
|
|
947
|
+
status: this.toStatus(order.status),
|
|
948
|
+
tif: order.tif,
|
|
949
|
+
reduceOnly: order.reduceOnly,
|
|
950
|
+
placedAt: order.placedAt,
|
|
951
|
+
xtras: order.xtras
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
TradingService = __decorateClass([
|
|
956
|
+
Injectable()
|
|
957
|
+
], TradingService);
|
|
958
|
+
var WalletService = class {
|
|
959
|
+
logger = new Logger(WalletService.name);
|
|
960
|
+
/** Les venues dont le portefeuille est lisible aujourd'hui. */
|
|
961
|
+
venues() {
|
|
962
|
+
return WALLET_XEXES;
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Les soldes d'un ou plusieurs comptes.
|
|
966
|
+
*
|
|
967
|
+
* Même politique d'échec que les autres services : sur un seul accès, l'échec est relancé ; sur
|
|
968
|
+
* plusieurs, la liste partielle garde sa valeur et l'incident est journalisé. Rendre `[]` en
|
|
969
|
+
* silence ferait passer une venue injoignable pour un compte vide — et un compte vide, ça se
|
|
970
|
+
* regarde autrement qu'une panne.
|
|
971
|
+
*/
|
|
972
|
+
async balances(...accesses) {
|
|
973
|
+
return this.collect(accesses, "balances", async (access) => {
|
|
974
|
+
const balances = await createAccountXex(access.xex, access).account().getBalances();
|
|
975
|
+
return balances.map((balance) => ({
|
|
976
|
+
xex: access.xex,
|
|
977
|
+
asset: balance.asset,
|
|
978
|
+
total: balance.total,
|
|
979
|
+
available: balance.available,
|
|
980
|
+
usdValue: balance.usdValue,
|
|
981
|
+
xtras: balance.xtras
|
|
982
|
+
}));
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Les mouvements d'argent, triés du plus ancien au plus récent, montants **signés**.
|
|
987
|
+
*
|
|
988
|
+
* **Lève si la venue ne les sert pas** plutôt que de rendre une liste vide : « cette venue
|
|
989
|
+
* n'expose pas son historique » et « ce compte n'a jamais bougé » appellent des suites très
|
|
990
|
+
* différentes.
|
|
991
|
+
*/
|
|
992
|
+
async movements(...accesses) {
|
|
993
|
+
const collected = await this.collect(accesses, "movements", async (access) => {
|
|
994
|
+
const account = createAccountXex(access.xex, access).account();
|
|
995
|
+
if (account.getMovements === void 0) {
|
|
996
|
+
throw new Error(`movements(${access.xex}) : cette venue n'expose pas ses mouvements.`);
|
|
997
|
+
}
|
|
998
|
+
const movements = await account.getMovements();
|
|
999
|
+
return movements.map((movement) => ({
|
|
1000
|
+
xex: access.xex,
|
|
1001
|
+
kind: movement.kind,
|
|
1002
|
+
amount: movement.amount,
|
|
1003
|
+
fee: movement.fee,
|
|
1004
|
+
asset: movement.asset,
|
|
1005
|
+
occurredAt: movement.occurredAt,
|
|
1006
|
+
status: movement.status,
|
|
1007
|
+
externalId: movement.externalId,
|
|
1008
|
+
xtras: movement.xtras
|
|
1009
|
+
}));
|
|
1010
|
+
});
|
|
1011
|
+
return collected.sort((left, right) => left.occurredAt.getTime() - right.occurredAt.getTime());
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* La valeur du compte dans le temps.
|
|
1015
|
+
*
|
|
1016
|
+
* **Seules hyperliquid et pacifica la publient**, et XGate ne la fabrique pas ailleurs :
|
|
1017
|
+
* reconstituer une courbe depuis les mouvements et les prix donnerait une valeur qu'aucune venue
|
|
1018
|
+
* n'a cotée, avec une méthode incomparable à celle des deux qui la publient. Lève sur une venue
|
|
1019
|
+
* qui n'en sert pas.
|
|
1020
|
+
*/
|
|
1021
|
+
async equityHistory(...accesses) {
|
|
1022
|
+
const collected = await this.collect(accesses, "equityHistory", async (access) => {
|
|
1023
|
+
const account = createAccountXex(access.xex, access).account();
|
|
1024
|
+
if (account.getEquityHistory === void 0) {
|
|
1025
|
+
throw new Error(
|
|
1026
|
+
`equityHistory(${access.xex}) : cette venue ne publie pas la valeur de compte dans le temps.`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
const points = await account.getEquityHistory();
|
|
1030
|
+
return points.map((point) => ({
|
|
1031
|
+
xex: access.xex,
|
|
1032
|
+
measuredAt: point.measuredAt,
|
|
1033
|
+
equity: point.equity
|
|
1034
|
+
}));
|
|
1035
|
+
});
|
|
1036
|
+
return collected.sort((left, right) => left.measuredAt.getTime() - right.measuredAt.getTime());
|
|
1037
|
+
}
|
|
1038
|
+
/** Le motif commun aux trois lectures : un accès seul relance, plusieurs journalisent. */
|
|
1039
|
+
async collect(accesses, capacite, lire) {
|
|
1040
|
+
if (accesses.length === 0) {
|
|
1041
|
+
throw new Error(`${capacite}() : aucun acc\xE8s fourni \u2014 pr\xE9cise au moins une venue.`);
|
|
1042
|
+
}
|
|
1043
|
+
const solo = accesses.length === 1;
|
|
1044
|
+
const collected = await Promise.all(
|
|
1045
|
+
accesses.map(async (access) => {
|
|
1046
|
+
try {
|
|
1047
|
+
return await lire(access);
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
if (solo === true) {
|
|
1050
|
+
throw error;
|
|
1051
|
+
}
|
|
1052
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1053
|
+
this.logger.error(`${capacite}(${access.xex}) a \xE9chou\xE9, venue ignor\xE9e : ${message}`);
|
|
1054
|
+
return [];
|
|
1055
|
+
}
|
|
1056
|
+
})
|
|
1057
|
+
);
|
|
1058
|
+
return collected.flat();
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
WalletService = __decorateClass([
|
|
1062
|
+
Injectable()
|
|
1063
|
+
], WalletService);
|
|
1064
|
+
var WsCandlesService = class {
|
|
1065
|
+
logger = new Logger(WsCandlesService.name);
|
|
1066
|
+
/**
|
|
1067
|
+
* Souscrit aux bougies d'un marché chez une venue. Rend la fonction de désabonnement.
|
|
1068
|
+
*
|
|
1069
|
+
* Le handler reçoit **une bougie à la fois**, au format unifié — c'est la forme commune aux neuf
|
|
1070
|
+
* SDK. La bougie en cours est repoussée à chaque mise à jour tant qu'elle n'est pas close ;
|
|
1071
|
+
* `closedAt` dit jusqu'où elle porte.
|
|
1072
|
+
*
|
|
1073
|
+
* **Attention à extended** : sur un intervalle qu'elle ne connaît pas, sa façade rend un
|
|
1074
|
+
* désabonnement vide au lieu de lever. L'appelant se croit abonné et n'entend jamais rien — un
|
|
1075
|
+
* silence indiscernable d'un marché sans échange. C'est un défaut du SDK, pas d'XGate, et il est
|
|
1076
|
+
* dans la file des corrections.
|
|
1077
|
+
*/
|
|
1078
|
+
subscribe(xex, query, handler) {
|
|
1079
|
+
const quote = this.quoteOf(query.symbol);
|
|
1080
|
+
this.logger.log(`souscription bougies ${query.symbol} ${query.interval} sur ${xex}`);
|
|
1081
|
+
return createPublicXex(xex).ws().subscribeCandles({ name: query.symbol, interval: query.interval }, (wire) => {
|
|
1082
|
+
handler(toCandle(wire, xex, query.interval, quote));
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Souscrit au MÊME marché chez plusieurs venues, avec un seul handler.
|
|
1087
|
+
*
|
|
1088
|
+
* Chaque bougie porte son `xex` : l'appelant sait toujours qui parle. La fonction rendue coupe
|
|
1089
|
+
* tous les flux d'un coup — sinon il faudrait en garder N et n'en oublier aucun.
|
|
1090
|
+
*/
|
|
1091
|
+
subscribeAll(xexes, query, handler) {
|
|
1092
|
+
if (xexes.length === 0) {
|
|
1093
|
+
throw new Error("subscribeAll() : aucune venue demand\xE9e \u2014 pr\xE9cise au moins un XgateEx.");
|
|
1094
|
+
}
|
|
1095
|
+
const stops = [];
|
|
1096
|
+
for (const xex of xexes) {
|
|
1097
|
+
try {
|
|
1098
|
+
stops.push(this.subscribe(xex, query, handler));
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1101
|
+
this.logger.error(`souscription ${xex} refus\xE9e, venue ignor\xE9e : ${message}`);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return () => {
|
|
1105
|
+
for (const stop of stops) {
|
|
1106
|
+
stop();
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
/** Voir `CandlesService.quoteOf` : la cotation se déduit du symbole natif, faute d'être publiée. */
|
|
1111
|
+
quoteOf(symbolXex) {
|
|
1112
|
+
const parts = symbolXex.split("-");
|
|
1113
|
+
const last = parts[parts.length - 1];
|
|
1114
|
+
return parts.length > 1 && last !== void 0 ? last : "";
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
WsCandlesService = __decorateClass([
|
|
1118
|
+
Injectable()
|
|
1119
|
+
], WsCandlesService);
|
|
1120
|
+
|
|
1121
|
+
// src/xgate.module.ts
|
|
1122
|
+
var XgateModule = class {
|
|
1123
|
+
};
|
|
1124
|
+
XgateModule = __decorateClass([
|
|
1125
|
+
Module({
|
|
1126
|
+
providers: [
|
|
1127
|
+
CatalogService,
|
|
1128
|
+
CandlesService,
|
|
1129
|
+
WsCandlesService,
|
|
1130
|
+
PricesService,
|
|
1131
|
+
SpotCatalogService,
|
|
1132
|
+
WalletService
|
|
1133
|
+
],
|
|
1134
|
+
exports: [
|
|
1135
|
+
CatalogService,
|
|
1136
|
+
CandlesService,
|
|
1137
|
+
WsCandlesService,
|
|
1138
|
+
PricesService,
|
|
1139
|
+
SpotCatalogService,
|
|
1140
|
+
WalletService
|
|
1141
|
+
]
|
|
1142
|
+
})
|
|
1143
|
+
], XgateModule);
|
|
1144
|
+
|
|
1145
|
+
export { CandlesService, CatalogService, ORDER_STATUSES, PricesService, SPOT_XEXES, SpotCatalogService, TIMEFRAME_MINUTES, Timeframe, TradingService, WALLET_XEXES, WalletService, WsCandlesService, XGATE_EXCHANGES, XgateEx, XgateModule, buildLevels, buildProtection, canonicalFromBase, canonicalFromXex, roundPrice, roundToStep, servesNatively, servesSpot, sourceFor, timeframeMs };
|
|
1146
|
+
//# sourceMappingURL=index.js.map
|
|
1147
|
+
//# sourceMappingURL=index.js.map
|