@danielgroen/dxtrade-api 1.0.25 → 1.0.26
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 +2 -1
- package/dist/index.d.mts +124 -89
- package/dist/index.d.ts +124 -89
- package/dist/index.js +1063 -848
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1060 -848
- package/dist/index.mjs.map +1 -1
- package/llms.txt +2 -1
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -70,7 +70,9 @@ var ERROR = /* @__PURE__ */ ((ERROR2) => {
|
|
|
70
70
|
ERROR2["ORDERS_TIMEOUT"] = "ORDERS_TIMEOUT";
|
|
71
71
|
ERROR2["ORDERS_ERROR"] = "ORDERS_ERROR";
|
|
72
72
|
ERROR2["CANCEL_ORDER_ERROR"] = "CANCEL_ORDER_ERROR";
|
|
73
|
+
ERROR2["POSITION_NOT_FOUND"] = "POSITION_NOT_FOUND";
|
|
73
74
|
ERROR2["POSITION_CLOSE_ERROR"] = "POSITION_CLOSE_ERROR";
|
|
75
|
+
ERROR2["POSITION_CLOSE_TIMEOUT"] = "POSITION_CLOSE_TIMEOUT";
|
|
74
76
|
ERROR2["POSITION_METRICS_TIMEOUT"] = "POSITION_METRICS_TIMEOUT";
|
|
75
77
|
ERROR2["POSITION_METRICS_ERROR"] = "POSITION_METRICS_ERROR";
|
|
76
78
|
ERROR2["ACCOUNT_METRICS_TIMEOUT"] = "ACCOUNT_METRICS_TIMEOUT";
|
|
@@ -85,6 +87,22 @@ var ERROR = /* @__PURE__ */ ((ERROR2) => {
|
|
|
85
87
|
ERROR2["STREAM_REQUIRES_CONNECT"] = "STREAM_REQUIRES_CONNECT";
|
|
86
88
|
return ERROR2;
|
|
87
89
|
})(ERROR || {});
|
|
90
|
+
var MESSAGE_CATEGORY = /* @__PURE__ */ ((MESSAGE_CATEGORY2) => {
|
|
91
|
+
MESSAGE_CATEGORY2["TRADE_LOG"] = "TRADE_LOG";
|
|
92
|
+
MESSAGE_CATEGORY2["NOTIFICATION"] = "NOTIFICATION";
|
|
93
|
+
return MESSAGE_CATEGORY2;
|
|
94
|
+
})(MESSAGE_CATEGORY || {});
|
|
95
|
+
var MESSAGE_TYPE = /* @__PURE__ */ ((MESSAGE_TYPE2) => {
|
|
96
|
+
MESSAGE_TYPE2["ORDER"] = "ORDER";
|
|
97
|
+
MESSAGE_TYPE2["INSTRUMENT_ACTIVATED"] = "INSTRUMENT_ACTIVATED";
|
|
98
|
+
return MESSAGE_TYPE2;
|
|
99
|
+
})(MESSAGE_TYPE || {});
|
|
100
|
+
var ORDER_STATUS = /* @__PURE__ */ ((ORDER_STATUS2) => {
|
|
101
|
+
ORDER_STATUS2["PLACED"] = "PLACED";
|
|
102
|
+
ORDER_STATUS2["FILLED"] = "FILLED";
|
|
103
|
+
ORDER_STATUS2["REJECTED"] = "REJECTED";
|
|
104
|
+
return ORDER_STATUS2;
|
|
105
|
+
})(ORDER_STATUS || {});
|
|
88
106
|
var WS_MESSAGE = /* @__PURE__ */ ((WS_MESSAGE2) => {
|
|
89
107
|
WS_MESSAGE2["ACCOUNT_METRICS"] = "ACCOUNT_METRICS";
|
|
90
108
|
WS_MESSAGE2["ACCOUNTS"] = "ACCOUNTS";
|
|
@@ -150,7 +168,10 @@ var Cookies = class {
|
|
|
150
168
|
function baseHeaders() {
|
|
151
169
|
return {
|
|
152
170
|
"Content-Type": "application/json; charset=UTF-8",
|
|
153
|
-
|
|
171
|
+
Accept: "*/*",
|
|
172
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
173
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
174
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:147.0) Gecko/20100101 Firefox/147.0"
|
|
154
175
|
};
|
|
155
176
|
}
|
|
156
177
|
function authHeaders(csrf, cookieStr) {
|
|
@@ -176,7 +197,11 @@ async function retryRequest(config, retries = 3) {
|
|
|
176
197
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
177
198
|
console.warn(`[dxtrade-api] Attempt ${attempt} failed: ${message}`, config.url);
|
|
178
199
|
if (isAxiosError(error) && error.response?.status === 429) {
|
|
179
|
-
|
|
200
|
+
if (attempt === retries) {
|
|
201
|
+
throw new DxtradeError("RATE_LIMITED" /* RATE_LIMITED */, "Rate limited (429). Too many requests \u2014 try again later.");
|
|
202
|
+
}
|
|
203
|
+
await new Promise((res) => setTimeout(res, 3e3 * attempt));
|
|
204
|
+
continue;
|
|
180
205
|
}
|
|
181
206
|
if (attempt === retries) throw error;
|
|
182
207
|
await new Promise((res) => setTimeout(res, 1e3 * attempt));
|
|
@@ -209,6 +234,11 @@ function parseAtmosphereId(data) {
|
|
|
209
234
|
}
|
|
210
235
|
return null;
|
|
211
236
|
}
|
|
237
|
+
function checkWsRateLimit(error) {
|
|
238
|
+
if (error.message.includes("429")) {
|
|
239
|
+
throw new DxtradeError("RATE_LIMITED" /* RATE_LIMITED */, "Rate limited (429). Too many requests \u2014 try again later.");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
212
242
|
function parseWsData(data) {
|
|
213
243
|
const raw = data.toString();
|
|
214
244
|
const pipeIndex = raw.indexOf("|");
|
|
@@ -242,10 +272,12 @@ var WsManager = class extends EventEmitter {
|
|
|
242
272
|
this.emit(payload.type, payload.body);
|
|
243
273
|
});
|
|
244
274
|
ws.on("error", (error) => {
|
|
275
|
+
checkWsRateLimit(error);
|
|
276
|
+
const err = new Error(`WebSocket manager error: ${error.message}`);
|
|
245
277
|
if (!this._ws) {
|
|
246
|
-
return reject(
|
|
278
|
+
return reject(err);
|
|
247
279
|
}
|
|
248
|
-
this.emit("error",
|
|
280
|
+
this.emit("error", err);
|
|
249
281
|
});
|
|
250
282
|
ws.on("close", () => {
|
|
251
283
|
this._ws = null;
|
|
@@ -287,347 +319,395 @@ var WsManager = class extends EventEmitter {
|
|
|
287
319
|
};
|
|
288
320
|
|
|
289
321
|
// src/domains/account/account.ts
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const body = await ctx.wsManager.waitFor("ACCOUNT_METRICS" /* ACCOUNT_METRICS */, timeout);
|
|
294
|
-
return body.allMetrics;
|
|
322
|
+
var AccountDomain = class {
|
|
323
|
+
constructor(_ctx) {
|
|
324
|
+
this._ctx = _ctx;
|
|
295
325
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
326
|
+
/** Get account metrics including equity, balance, margin, and open P&L. */
|
|
327
|
+
async metrics(timeout = 3e4) {
|
|
328
|
+
this._ctx.ensureSession();
|
|
329
|
+
if (this._ctx.wsManager) {
|
|
330
|
+
const body = await this._ctx.wsManager.waitFor(
|
|
331
|
+
"ACCOUNT_METRICS" /* ACCOUNT_METRICS */,
|
|
332
|
+
timeout
|
|
333
|
+
);
|
|
334
|
+
return body.allMetrics;
|
|
335
|
+
}
|
|
336
|
+
const wsUrl = endpoints.websocket(this._ctx.broker, this._ctx.atmosphereId);
|
|
337
|
+
const cookieStr = Cookies.serialize(this._ctx.cookies);
|
|
338
|
+
return new Promise((resolve, reject) => {
|
|
339
|
+
const ws = new WebSocket2(wsUrl, { headers: { Cookie: cookieStr } });
|
|
340
|
+
const timer = setTimeout(() => {
|
|
341
|
+
ws.close();
|
|
342
|
+
reject(new DxtradeError("ACCOUNT_METRICS_TIMEOUT" /* ACCOUNT_METRICS_TIMEOUT */, "Account metrics timed out"));
|
|
343
|
+
}, timeout);
|
|
344
|
+
ws.on("message", (data) => {
|
|
345
|
+
const msg = parseWsData(data);
|
|
346
|
+
if (shouldLog(msg, this._ctx.debug)) debugLog(msg);
|
|
347
|
+
if (typeof msg === "string") return;
|
|
348
|
+
if (msg.type === "ACCOUNT_METRICS" /* ACCOUNT_METRICS */) {
|
|
349
|
+
clearTimeout(timer);
|
|
350
|
+
ws.close();
|
|
351
|
+
const body = msg.body;
|
|
352
|
+
resolve(body.allMetrics);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
ws.on("error", (error) => {
|
|
309
356
|
clearTimeout(timer);
|
|
310
357
|
ws.close();
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
ws.on("error", (error) => {
|
|
316
|
-
clearTimeout(timer);
|
|
317
|
-
ws.close();
|
|
318
|
-
reject(new DxtradeError("ACCOUNT_METRICS_ERROR" /* ACCOUNT_METRICS_ERROR */, `Account metrics error: ${error.message}`));
|
|
358
|
+
checkWsRateLimit(error);
|
|
359
|
+
reject(new DxtradeError("ACCOUNT_METRICS_ERROR" /* ACCOUNT_METRICS_ERROR */, `Account metrics error: ${error.message}`));
|
|
360
|
+
});
|
|
319
361
|
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Fetch trade history for a date range.
|
|
365
|
+
* @param params.from - Start timestamp (Unix ms)
|
|
366
|
+
* @param params.to - End timestamp (Unix ms)
|
|
367
|
+
*/
|
|
368
|
+
async tradeHistory(params) {
|
|
369
|
+
this._ctx.ensureSession();
|
|
370
|
+
try {
|
|
371
|
+
const response = await retryRequest(
|
|
372
|
+
{
|
|
373
|
+
method: "POST",
|
|
374
|
+
url: endpoints.tradeHistory(this._ctx.broker, params),
|
|
375
|
+
headers: authHeaders(this._ctx.csrf, Cookies.serialize(this._ctx.cookies))
|
|
376
|
+
},
|
|
377
|
+
this._ctx.retries
|
|
378
|
+
);
|
|
379
|
+
if (response.status === 200) {
|
|
380
|
+
const setCookies = response.headers["set-cookie"] ?? [];
|
|
381
|
+
const incoming = Cookies.parse(setCookies);
|
|
382
|
+
this._ctx.cookies = Cookies.merge(this._ctx.cookies, incoming);
|
|
383
|
+
return response.data;
|
|
384
|
+
} else {
|
|
385
|
+
this._ctx.throwError("TRADE_HISTORY_ERROR" /* TRADE_HISTORY_ERROR */, `Trade history failed: ${response.status}`);
|
|
386
|
+
}
|
|
387
|
+
} catch (error) {
|
|
388
|
+
if (error instanceof DxtradeError) throw error;
|
|
389
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
390
|
+
this._ctx.throwError("TRADE_HISTORY_ERROR" /* TRADE_HISTORY_ERROR */, `Trade history error: ${message}`);
|
|
340
391
|
}
|
|
341
|
-
} catch (error) {
|
|
342
|
-
if (error instanceof DxtradeError) throw error;
|
|
343
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
344
|
-
ctx.throwError("TRADE_HISTORY_ERROR" /* TRADE_HISTORY_ERROR */, `Trade history error: ${message}`);
|
|
345
392
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
393
|
+
/**
|
|
394
|
+
* Fetch trade journal entries for a date range.
|
|
395
|
+
* @param params.from - Start timestamp (Unix ms)
|
|
396
|
+
* @param params.to - End timestamp (Unix ms)
|
|
397
|
+
*/
|
|
398
|
+
async tradeJournal(params) {
|
|
399
|
+
this._ctx.ensureSession();
|
|
400
|
+
try {
|
|
401
|
+
const cookieStr = Cookies.serialize(this._ctx.cookies);
|
|
402
|
+
const response = await retryRequest(
|
|
403
|
+
{
|
|
404
|
+
method: "GET",
|
|
405
|
+
url: endpoints.tradeJournal(this._ctx.broker, params),
|
|
406
|
+
headers: { ...baseHeaders(), Cookie: cookieStr }
|
|
407
|
+
},
|
|
408
|
+
this._ctx.retries
|
|
409
|
+
);
|
|
410
|
+
if (response.status === 200) {
|
|
411
|
+
const setCookies = response.headers["set-cookie"] ?? [];
|
|
412
|
+
const incoming = Cookies.parse(setCookies);
|
|
413
|
+
this._ctx.cookies = Cookies.merge(this._ctx.cookies, incoming);
|
|
414
|
+
return response.data;
|
|
415
|
+
} else {
|
|
416
|
+
this._ctx.throwError("TRADE_JOURNAL_ERROR" /* TRADE_JOURNAL_ERROR */, `Login failed: ${response.status}`);
|
|
417
|
+
}
|
|
418
|
+
} catch (error) {
|
|
419
|
+
if (error instanceof DxtradeError) throw error;
|
|
420
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
421
|
+
this._ctx.throwError("TRADE_JOURNAL_ERROR" /* TRADE_JOURNAL_ERROR */, `Trade journal error: ${message}`);
|
|
366
422
|
}
|
|
367
|
-
} catch (error) {
|
|
368
|
-
if (error instanceof DxtradeError) throw error;
|
|
369
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
370
|
-
ctx.throwError("TRADE_JOURNAL_ERROR" /* TRADE_JOURNAL_ERROR */, `Trade journal error: ${message}`);
|
|
371
423
|
}
|
|
372
|
-
}
|
|
424
|
+
};
|
|
373
425
|
|
|
374
426
|
// src/domains/assessments/assessments.ts
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
427
|
+
var AssessmentsDomain = class {
|
|
428
|
+
constructor(_ctx) {
|
|
429
|
+
this._ctx = _ctx;
|
|
430
|
+
}
|
|
431
|
+
/** Fetch PnL assessments for an instrument within a date range. */
|
|
432
|
+
async get(params) {
|
|
433
|
+
this._ctx.ensureSession();
|
|
434
|
+
try {
|
|
435
|
+
const response = await retryRequest(
|
|
436
|
+
{
|
|
437
|
+
method: "POST",
|
|
438
|
+
url: endpoints.assessments(this._ctx.broker),
|
|
439
|
+
data: {
|
|
440
|
+
from: params.from,
|
|
441
|
+
instrument: params.instrument,
|
|
442
|
+
subtype: params.subtype ?? null,
|
|
443
|
+
to: params.to
|
|
444
|
+
},
|
|
445
|
+
headers: authHeaders(this._ctx.csrf, Cookies.serialize(this._ctx.cookies))
|
|
387
446
|
},
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
)
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
ctx.throwError("ASSESSMENTS_ERROR" /* ASSESSMENTS_ERROR */, `Error fetching assessments: ${message}`);
|
|
447
|
+
this._ctx.retries
|
|
448
|
+
);
|
|
449
|
+
return response.data;
|
|
450
|
+
} catch (error) {
|
|
451
|
+
if (error instanceof DxtradeError) throw error;
|
|
452
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
453
|
+
this._ctx.throwError("ASSESSMENTS_ERROR" /* ASSESSMENTS_ERROR */, `Error fetching assessments: ${message}`);
|
|
454
|
+
}
|
|
397
455
|
}
|
|
398
|
-
}
|
|
456
|
+
};
|
|
399
457
|
|
|
400
458
|
// src/domains/instrument/instrument.ts
|
|
401
459
|
import WebSocket3 from "ws";
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
460
|
+
var InstrumentsDomain = class {
|
|
461
|
+
constructor(_ctx) {
|
|
462
|
+
this._ctx = _ctx;
|
|
463
|
+
}
|
|
464
|
+
/** Get all available instruments, optionally filtered by partial match (e.g. `{ type: "FOREX" }`). */
|
|
465
|
+
async get(params = {}, timeout = 3e4) {
|
|
466
|
+
this._ctx.ensureSession();
|
|
467
|
+
const wsUrl = endpoints.websocket(this._ctx.broker, this._ctx.atmosphereId);
|
|
468
|
+
const cookieStr = Cookies.serialize(this._ctx.cookies);
|
|
469
|
+
return new Promise((resolve, reject) => {
|
|
470
|
+
const ws = new WebSocket3(wsUrl, { headers: { Cookie: cookieStr } });
|
|
471
|
+
const timer = setTimeout(() => {
|
|
472
|
+
ws.close();
|
|
473
|
+
reject(new DxtradeError("INSTRUMENTS_TIMEOUT" /* INSTRUMENTS_TIMEOUT */, "Instruments request timed out"));
|
|
474
|
+
}, timeout);
|
|
475
|
+
let instruments = [];
|
|
476
|
+
let settleTimer = null;
|
|
477
|
+
ws.on("message", (data) => {
|
|
478
|
+
const msg = parseWsData(data);
|
|
479
|
+
if (shouldLog(msg, this._ctx.debug)) debugLog(msg);
|
|
480
|
+
if (typeof msg === "string") return;
|
|
481
|
+
if (msg.type === "INSTRUMENTS" /* INSTRUMENTS */) {
|
|
482
|
+
instruments.push(...msg.body);
|
|
483
|
+
if (settleTimer) clearTimeout(settleTimer);
|
|
484
|
+
settleTimer = setTimeout(() => {
|
|
485
|
+
clearTimeout(timer);
|
|
486
|
+
ws.close();
|
|
487
|
+
resolve(
|
|
488
|
+
instruments.filter((instrument) => {
|
|
489
|
+
for (const key in params) {
|
|
490
|
+
if (params[key] !== instrument[key]) {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
429
493
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
494
|
+
return true;
|
|
495
|
+
})
|
|
496
|
+
);
|
|
497
|
+
}, 200);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
ws.on("error", (error) => {
|
|
501
|
+
clearTimeout(timer);
|
|
502
|
+
ws.close();
|
|
503
|
+
checkWsRateLimit(error);
|
|
504
|
+
reject(new DxtradeError("INSTRUMENTS_ERROR" /* INSTRUMENTS_ERROR */, `Instruments error: ${error.message}`));
|
|
505
|
+
});
|
|
441
506
|
});
|
|
442
|
-
}
|
|
443
|
-
}
|
|
507
|
+
}
|
|
508
|
+
};
|
|
444
509
|
|
|
445
510
|
// src/domains/ohlc/ohlc.ts
|
|
446
511
|
import WebSocket4 from "ws";
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
"STREAM_REQUIRES_CONNECT" /* STREAM_REQUIRES_CONNECT */,
|
|
451
|
-
"Streaming requires a persistent WebSocket. Use connect() instead of auth()."
|
|
452
|
-
);
|
|
512
|
+
var OhlcDomain = class {
|
|
513
|
+
constructor(_ctx) {
|
|
514
|
+
this._ctx = _ctx;
|
|
453
515
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (body?.subtopic !== subtopic) return;
|
|
462
|
-
const data = body.data;
|
|
463
|
-
if (!Array.isArray(data)) return;
|
|
464
|
-
if (!snapshotDone) {
|
|
465
|
-
snapshotBars.push(...data);
|
|
466
|
-
if (body.snapshotEnd) {
|
|
467
|
-
snapshotDone = true;
|
|
468
|
-
callback([...snapshotBars]);
|
|
469
|
-
resolveSnapshot?.();
|
|
470
|
-
}
|
|
471
|
-
} else {
|
|
472
|
-
callback(data);
|
|
516
|
+
/** Stream real-time OHLC bar updates. Requires connect(). Returns unsubscribe function. */
|
|
517
|
+
async stream(params, callback) {
|
|
518
|
+
if (!this._ctx.wsManager) {
|
|
519
|
+
this._ctx.throwError(
|
|
520
|
+
"STREAM_REQUIRES_CONNECT" /* STREAM_REQUIRES_CONNECT */,
|
|
521
|
+
"Streaming requires a persistent WebSocket. Use connect() instead of auth()."
|
|
522
|
+
);
|
|
473
523
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
chartIds: [],
|
|
492
|
-
requests: [
|
|
493
|
-
{
|
|
494
|
-
aggregationPeriodSeconds: resolution,
|
|
495
|
-
extendedSession: true,
|
|
496
|
-
forexPriceField: priceField,
|
|
497
|
-
id: 0,
|
|
498
|
-
maxBarsCount: maxBars,
|
|
499
|
-
range,
|
|
500
|
-
studySubscription: [],
|
|
501
|
-
subtopic,
|
|
502
|
-
symbol
|
|
503
|
-
}
|
|
504
|
-
]
|
|
505
|
-
},
|
|
506
|
-
headers
|
|
507
|
-
},
|
|
508
|
-
ctx.retries
|
|
509
|
-
);
|
|
510
|
-
} catch (error) {
|
|
511
|
-
ctx.wsManager.removeListener("chartFeedSubtopic" /* CHART_FEED_SUBTOPIC */, onChartFeed);
|
|
512
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
513
|
-
ctx.throwError("OHLC_ERROR" /* OHLC_ERROR */, `OHLC stream subscription error: ${message}`);
|
|
514
|
-
}
|
|
515
|
-
await new Promise((resolve, reject) => {
|
|
516
|
-
if (snapshotDone) return resolve();
|
|
517
|
-
const timer = setTimeout(() => {
|
|
518
|
-
if (snapshotBars.length > 0) {
|
|
519
|
-
snapshotDone = true;
|
|
520
|
-
callback([...snapshotBars]);
|
|
521
|
-
resolve();
|
|
524
|
+
const { symbol, resolution = 60, range = 432e3, maxBars = 3500, priceField = "bid" } = params;
|
|
525
|
+
const subtopic = WS_MESSAGE.SUBTOPIC.OHLC_STREAM;
|
|
526
|
+
const headers = authHeaders(this._ctx.csrf, Cookies.serialize(this._ctx.cookies));
|
|
527
|
+
const snapshotBars = [];
|
|
528
|
+
let snapshotDone = false;
|
|
529
|
+
let resolveSnapshot = null;
|
|
530
|
+
const onChartFeed = (body) => {
|
|
531
|
+
if (body?.subtopic !== subtopic) return;
|
|
532
|
+
const data = body.data;
|
|
533
|
+
if (!Array.isArray(data)) return;
|
|
534
|
+
if (!snapshotDone) {
|
|
535
|
+
snapshotBars.push(...data);
|
|
536
|
+
if (body.snapshotEnd) {
|
|
537
|
+
snapshotDone = true;
|
|
538
|
+
callback([...snapshotBars]);
|
|
539
|
+
resolveSnapshot?.();
|
|
540
|
+
}
|
|
522
541
|
} else {
|
|
523
|
-
|
|
524
|
-
reject(new DxtradeError("OHLC_TIMEOUT" /* OHLC_TIMEOUT */, "OHLC stream snapshot timed out"));
|
|
542
|
+
callback(data);
|
|
525
543
|
}
|
|
526
|
-
}, 3e4);
|
|
527
|
-
resolveSnapshot = () => {
|
|
528
|
-
clearTimeout(timer);
|
|
529
|
-
resolve();
|
|
530
544
|
};
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
await retryRequest(
|
|
562
|
-
{
|
|
563
|
-
method: "PUT",
|
|
564
|
-
url: endpoints.subscribeInstruments(ctx.broker),
|
|
565
|
-
data: { instruments: [symbol] },
|
|
566
|
-
headers
|
|
567
|
-
},
|
|
568
|
-
ctx.retries
|
|
569
|
-
);
|
|
570
|
-
await retryRequest(
|
|
571
|
-
{
|
|
572
|
-
method: "PUT",
|
|
573
|
-
url: endpoints.charts(ctx.broker),
|
|
574
|
-
data: {
|
|
575
|
-
chartIds: [],
|
|
576
|
-
requests: [
|
|
577
|
-
{
|
|
578
|
-
aggregationPeriodSeconds: resolution,
|
|
579
|
-
extendedSession: true,
|
|
580
|
-
forexPriceField: priceField,
|
|
581
|
-
id: 0,
|
|
582
|
-
maxBarsCount: maxBars,
|
|
583
|
-
range,
|
|
584
|
-
studySubscription: [],
|
|
585
|
-
subtopic: WS_MESSAGE.SUBTOPIC.BIG_CHART_COMPONENT,
|
|
586
|
-
symbol
|
|
587
|
-
}
|
|
588
|
-
]
|
|
589
|
-
},
|
|
590
|
-
headers
|
|
545
|
+
this._ctx.wsManager.on("chartFeedSubtopic" /* CHART_FEED_SUBTOPIC */, onChartFeed);
|
|
546
|
+
try {
|
|
547
|
+
await retryRequest(
|
|
548
|
+
{
|
|
549
|
+
method: "PUT",
|
|
550
|
+
url: endpoints.subscribeInstruments(this._ctx.broker),
|
|
551
|
+
data: { instruments: [symbol] },
|
|
552
|
+
headers
|
|
553
|
+
},
|
|
554
|
+
this._ctx.retries
|
|
555
|
+
);
|
|
556
|
+
await retryRequest(
|
|
557
|
+
{
|
|
558
|
+
method: "PUT",
|
|
559
|
+
url: endpoints.charts(this._ctx.broker),
|
|
560
|
+
data: {
|
|
561
|
+
chartIds: [],
|
|
562
|
+
requests: [
|
|
563
|
+
{
|
|
564
|
+
aggregationPeriodSeconds: resolution,
|
|
565
|
+
extendedSession: true,
|
|
566
|
+
forexPriceField: priceField,
|
|
567
|
+
id: 0,
|
|
568
|
+
maxBarsCount: maxBars,
|
|
569
|
+
range,
|
|
570
|
+
studySubscription: [],
|
|
571
|
+
subtopic,
|
|
572
|
+
symbol
|
|
573
|
+
}
|
|
574
|
+
]
|
|
591
575
|
},
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
576
|
+
headers
|
|
577
|
+
},
|
|
578
|
+
this._ctx.retries
|
|
579
|
+
);
|
|
580
|
+
} catch (error) {
|
|
581
|
+
this._ctx.wsManager.removeListener("chartFeedSubtopic" /* CHART_FEED_SUBTOPIC */, onChartFeed);
|
|
582
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
583
|
+
this._ctx.throwError("OHLC_ERROR" /* OHLC_ERROR */, `OHLC stream subscription error: ${message}`);
|
|
599
584
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
585
|
+
await new Promise((resolve, reject) => {
|
|
586
|
+
if (snapshotDone) return resolve();
|
|
587
|
+
const timer = setTimeout(() => {
|
|
588
|
+
if (snapshotBars.length > 0) {
|
|
589
|
+
snapshotDone = true;
|
|
590
|
+
callback([...snapshotBars]);
|
|
591
|
+
resolve();
|
|
592
|
+
} else {
|
|
593
|
+
this._ctx.wsManager?.removeListener("chartFeedSubtopic" /* CHART_FEED_SUBTOPIC */, onChartFeed);
|
|
594
|
+
reject(new DxtradeError("OHLC_TIMEOUT" /* OHLC_TIMEOUT */, "OHLC stream snapshot timed out"));
|
|
595
|
+
}
|
|
596
|
+
}, 3e4);
|
|
597
|
+
resolveSnapshot = () => {
|
|
598
|
+
clearTimeout(timer);
|
|
599
|
+
resolve();
|
|
600
|
+
};
|
|
601
|
+
});
|
|
602
|
+
return () => {
|
|
603
|
+
this._ctx.wsManager?.removeListener("chartFeedSubtopic" /* CHART_FEED_SUBTOPIC */, onChartFeed);
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Fetch OHLC price bars for a symbol.
|
|
608
|
+
* @param params.symbol - Instrument symbol (e.g. "EURUSD")
|
|
609
|
+
* @param params.resolution - Bar period in seconds (default: 60 = 1 min)
|
|
610
|
+
* @param params.range - Lookback window in seconds (default: 432000 = 5 days)
|
|
611
|
+
* @param params.maxBars - Maximum bars to return (default: 3500)
|
|
612
|
+
* @param params.priceField - "bid" or "ask" (default: "bid")
|
|
613
|
+
*/
|
|
614
|
+
async get(params, timeout = 3e4) {
|
|
615
|
+
this._ctx.ensureSession();
|
|
616
|
+
const { symbol, resolution = 60, range = 432e3, maxBars = 3500, priceField = "bid" } = params;
|
|
617
|
+
const wsUrl = endpoints.websocket(this._ctx.broker, this._ctx.atmosphereId);
|
|
618
|
+
const cookieStr = Cookies.serialize(this._ctx.cookies);
|
|
619
|
+
const headers = authHeaders(this._ctx.csrf, cookieStr);
|
|
620
|
+
return new Promise((resolve, reject) => {
|
|
621
|
+
const ws = new WebSocket4(wsUrl, { headers: { Cookie: cookieStr } });
|
|
622
|
+
const bars = [];
|
|
623
|
+
let putsSent = false;
|
|
624
|
+
let initSettleTimer = null;
|
|
625
|
+
let barSettleTimer = null;
|
|
626
|
+
const timer = setTimeout(() => {
|
|
627
|
+
ws.close();
|
|
628
|
+
reject(new DxtradeError("OHLC_TIMEOUT" /* OHLC_TIMEOUT */, "OHLC data timed out"));
|
|
629
|
+
}, timeout);
|
|
630
|
+
function cleanup() {
|
|
631
|
+
clearTimeout(timer);
|
|
605
632
|
if (initSettleTimer) clearTimeout(initSettleTimer);
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
609
|
-
const body = msg.body;
|
|
610
|
-
if (body?.subtopic !== WS_MESSAGE.SUBTOPIC.BIG_CHART_COMPONENT) return;
|
|
611
|
-
if (Array.isArray(body.data)) {
|
|
612
|
-
bars.push(...body.data);
|
|
633
|
+
if (barSettleTimer) clearTimeout(barSettleTimer);
|
|
634
|
+
ws.close();
|
|
613
635
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
636
|
+
const sendPuts = async () => {
|
|
637
|
+
putsSent = true;
|
|
638
|
+
try {
|
|
639
|
+
await retryRequest(
|
|
640
|
+
{
|
|
641
|
+
method: "PUT",
|
|
642
|
+
url: endpoints.subscribeInstruments(this._ctx.broker),
|
|
643
|
+
data: { instruments: [symbol] },
|
|
644
|
+
headers
|
|
645
|
+
},
|
|
646
|
+
this._ctx.retries
|
|
647
|
+
);
|
|
648
|
+
await retryRequest(
|
|
649
|
+
{
|
|
650
|
+
method: "PUT",
|
|
651
|
+
url: endpoints.charts(this._ctx.broker),
|
|
652
|
+
data: {
|
|
653
|
+
chartIds: [],
|
|
654
|
+
requests: [
|
|
655
|
+
{
|
|
656
|
+
aggregationPeriodSeconds: resolution,
|
|
657
|
+
extendedSession: true,
|
|
658
|
+
forexPriceField: priceField,
|
|
659
|
+
id: 0,
|
|
660
|
+
maxBarsCount: maxBars,
|
|
661
|
+
range,
|
|
662
|
+
studySubscription: [],
|
|
663
|
+
subtopic: WS_MESSAGE.SUBTOPIC.BIG_CHART_COMPONENT,
|
|
664
|
+
symbol
|
|
665
|
+
}
|
|
666
|
+
]
|
|
667
|
+
},
|
|
668
|
+
headers
|
|
669
|
+
},
|
|
670
|
+
this._ctx.retries
|
|
671
|
+
);
|
|
672
|
+
} catch (error) {
|
|
673
|
+
cleanup();
|
|
674
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
675
|
+
reject(new DxtradeError("OHLC_ERROR" /* OHLC_ERROR */, `Error fetching OHLC data: ${message}`));
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
ws.on("message", (data) => {
|
|
679
|
+
const msg = parseWsData(data);
|
|
680
|
+
if (shouldLog(msg, this._ctx.debug)) debugLog(msg);
|
|
681
|
+
if (typeof msg === "string") return;
|
|
682
|
+
if (!putsSent) {
|
|
683
|
+
if (initSettleTimer) clearTimeout(initSettleTimer);
|
|
684
|
+
initSettleTimer = setTimeout(() => sendPuts(), 1e3);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const body = msg.body;
|
|
688
|
+
if (body?.subtopic !== WS_MESSAGE.SUBTOPIC.BIG_CHART_COMPONENT) return;
|
|
689
|
+
if (Array.isArray(body.data)) {
|
|
690
|
+
bars.push(...body.data);
|
|
691
|
+
}
|
|
692
|
+
if (barSettleTimer) clearTimeout(barSettleTimer);
|
|
693
|
+
if (body.snapshotEnd) {
|
|
620
694
|
cleanup();
|
|
621
695
|
resolve(bars);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
696
|
+
} else {
|
|
697
|
+
barSettleTimer = setTimeout(() => {
|
|
698
|
+
cleanup();
|
|
699
|
+
resolve(bars);
|
|
700
|
+
}, 2e3);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
ws.on("error", (error) => {
|
|
704
|
+
cleanup();
|
|
705
|
+
checkWsRateLimit(error);
|
|
706
|
+
reject(new DxtradeError("OHLC_ERROR" /* OHLC_ERROR */, `OHLC WebSocket error: ${error.message}`));
|
|
707
|
+
});
|
|
628
708
|
});
|
|
629
|
-
}
|
|
630
|
-
}
|
|
709
|
+
}
|
|
710
|
+
};
|
|
631
711
|
|
|
632
712
|
// src/domains/order/order.ts
|
|
633
713
|
import crypto from "crypto";
|
|
@@ -635,87 +715,96 @@ import WebSocket6 from "ws";
|
|
|
635
715
|
|
|
636
716
|
// src/domains/symbol/symbol.ts
|
|
637
717
|
import WebSocket5 from "ws";
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
718
|
+
var SymbolsDomain = class {
|
|
719
|
+
constructor(_ctx) {
|
|
720
|
+
this._ctx = _ctx;
|
|
721
|
+
}
|
|
722
|
+
/** Search for symbols matching the given text (e.g. "EURUSD", "BTC"). */
|
|
723
|
+
async search(text) {
|
|
724
|
+
this._ctx.ensureSession();
|
|
725
|
+
try {
|
|
726
|
+
const cookieStr = Cookies.serialize(this._ctx.cookies);
|
|
727
|
+
const response = await retryRequest(
|
|
728
|
+
{
|
|
729
|
+
method: "GET",
|
|
730
|
+
url: endpoints.suggest(this._ctx.broker, text),
|
|
731
|
+
headers: { ...baseHeaders(), Cookie: cookieStr }
|
|
732
|
+
},
|
|
733
|
+
this._ctx.retries
|
|
734
|
+
);
|
|
735
|
+
const suggests = response.data?.suggests;
|
|
736
|
+
if (!suggests?.length) {
|
|
737
|
+
this._ctx.throwError("NO_SUGGESTIONS" /* NO_SUGGESTIONS */, "No symbol suggestions found");
|
|
738
|
+
}
|
|
739
|
+
return suggests;
|
|
740
|
+
} catch (error) {
|
|
741
|
+
if (error instanceof DxtradeError) throw error;
|
|
742
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
743
|
+
this._ctx.throwError("SUGGEST_ERROR" /* SUGGEST_ERROR */, `Error getting symbol suggestions: ${message}`);
|
|
653
744
|
}
|
|
654
|
-
return suggests;
|
|
655
|
-
} catch (error) {
|
|
656
|
-
if (error instanceof DxtradeError) throw error;
|
|
657
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
658
|
-
ctx.throwError("SUGGEST_ERROR" /* SUGGEST_ERROR */, `Error getting symbol suggestions: ${message}`);
|
|
659
745
|
}
|
|
660
|
-
|
|
661
|
-
async
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
746
|
+
/** Get detailed instrument info for a symbol, including volume limits and lot size. */
|
|
747
|
+
async info(symbol) {
|
|
748
|
+
this._ctx.ensureSession();
|
|
749
|
+
try {
|
|
750
|
+
const offsetMinutes = Math.abs((/* @__PURE__ */ new Date()).getTimezoneOffset());
|
|
751
|
+
const cookieStr = Cookies.serialize(this._ctx.cookies);
|
|
752
|
+
const response = await retryRequest(
|
|
753
|
+
{
|
|
754
|
+
method: "GET",
|
|
755
|
+
url: endpoints.instrumentInfo(this._ctx.broker, symbol, offsetMinutes),
|
|
756
|
+
headers: { ...baseHeaders(), Cookie: cookieStr }
|
|
757
|
+
},
|
|
758
|
+
this._ctx.retries
|
|
759
|
+
);
|
|
760
|
+
if (!response.data) {
|
|
761
|
+
this._ctx.throwError("NO_SYMBOL_INFO" /* NO_SYMBOL_INFO */, "No symbol info returned");
|
|
762
|
+
}
|
|
763
|
+
return response.data;
|
|
764
|
+
} catch (error) {
|
|
765
|
+
if (error instanceof DxtradeError) throw error;
|
|
766
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
767
|
+
this._ctx.throwError("SYMBOL_INFO_ERROR" /* SYMBOL_INFO_ERROR */, `Error getting symbol info: ${message}`);
|
|
676
768
|
}
|
|
677
|
-
return response.data;
|
|
678
|
-
} catch (error) {
|
|
679
|
-
if (error instanceof DxtradeError) throw error;
|
|
680
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
681
|
-
ctx.throwError("SYMBOL_INFO_ERROR" /* SYMBOL_INFO_ERROR */, `Error getting symbol info: ${message}`);
|
|
682
769
|
}
|
|
683
|
-
|
|
684
|
-
async
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
770
|
+
/** Get order size limits and stop/limit distances for all symbols. */
|
|
771
|
+
async limits(timeout = 3e4) {
|
|
772
|
+
this._ctx.ensureSession();
|
|
773
|
+
const wsUrl = endpoints.websocket(this._ctx.broker, this._ctx.atmosphereId);
|
|
774
|
+
const cookieStr = Cookies.serialize(this._ctx.cookies);
|
|
775
|
+
return new Promise((resolve, reject) => {
|
|
776
|
+
const ws = new WebSocket5(wsUrl, { headers: { Cookie: cookieStr } });
|
|
777
|
+
const timer = setTimeout(() => {
|
|
778
|
+
ws.close();
|
|
779
|
+
reject(new DxtradeError("LIMITS_TIMEOUT" /* LIMITS_TIMEOUT */, "Symbol limits request timed out"));
|
|
780
|
+
}, timeout);
|
|
781
|
+
let limits = [];
|
|
782
|
+
let settleTimer = null;
|
|
783
|
+
ws.on("message", (data) => {
|
|
784
|
+
const msg = parseWsData(data);
|
|
785
|
+
if (shouldLog(msg, this._ctx.debug)) debugLog(msg);
|
|
786
|
+
if (typeof msg === "string") return;
|
|
787
|
+
if (msg.type === "LIMITS" /* LIMITS */) {
|
|
788
|
+
const batch = msg.body;
|
|
789
|
+
if (batch.length === 0) return;
|
|
790
|
+
limits.push(...batch);
|
|
791
|
+
if (settleTimer) clearTimeout(settleTimer);
|
|
792
|
+
settleTimer = setTimeout(() => {
|
|
793
|
+
clearTimeout(timer);
|
|
794
|
+
ws.close();
|
|
795
|
+
resolve(limits);
|
|
796
|
+
}, 200);
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
ws.on("error", (error) => {
|
|
800
|
+
clearTimeout(timer);
|
|
801
|
+
ws.close();
|
|
802
|
+
checkWsRateLimit(error);
|
|
803
|
+
reject(new DxtradeError("LIMITS_ERROR" /* LIMITS_ERROR */, `Symbol limits error: ${error.message}`));
|
|
804
|
+
});
|
|
716
805
|
});
|
|
717
|
-
}
|
|
718
|
-
}
|
|
806
|
+
}
|
|
807
|
+
};
|
|
719
808
|
|
|
720
809
|
// src/domains/order/order.ts
|
|
721
810
|
function createOrderListener(wsUrl, cookieStr, timeout = 3e4, debug = false) {
|
|
@@ -746,20 +835,21 @@ function createOrderListener(wsUrl, cookieStr, timeout = 3e4, debug = false) {
|
|
|
746
835
|
if (msg.type === "MESSAGE" /* MESSAGE */) {
|
|
747
836
|
const messages = msg.body;
|
|
748
837
|
const orderMsg = messages?.findLast?.(
|
|
749
|
-
(m) => m.messageCategory === "TRADE_LOG" && m.messageType === "ORDER" && !m.historyMessage
|
|
838
|
+
(m) => m.messageCategory === "TRADE_LOG" /* TRADE_LOG */ && m.messageType === "ORDER" /* ORDER */ && !m.historyMessage
|
|
750
839
|
);
|
|
751
840
|
if (!orderMsg) return;
|
|
752
841
|
const params = orderMsg.parametersTO;
|
|
753
|
-
if (params.orderStatus === "REJECTED") {
|
|
842
|
+
if (params.orderStatus === "REJECTED" /* REJECTED */) {
|
|
754
843
|
const reason = params.rejectReason?.key ?? "Unknown reason";
|
|
755
844
|
done(new Error(`[dxtrade-api] Order rejected: ${reason}`));
|
|
756
|
-
} else if (params.orderStatus === "FILLED") {
|
|
845
|
+
} else if (params.orderStatus === "FILLED" /* FILLED */) {
|
|
757
846
|
done(null, {
|
|
758
847
|
orderId: params.orderKey,
|
|
759
848
|
status: params.orderStatus,
|
|
760
849
|
symbol: params.symbol,
|
|
761
850
|
filledQuantity: params.filledQuantity,
|
|
762
|
-
filledPrice: params.filledPrice
|
|
851
|
+
filledPrice: params.filledPrice,
|
|
852
|
+
positionCode: params.positionCode
|
|
763
853
|
});
|
|
764
854
|
}
|
|
765
855
|
return;
|
|
@@ -767,9 +857,9 @@ function createOrderListener(wsUrl, cookieStr, timeout = 3e4, debug = false) {
|
|
|
767
857
|
if (msg.type === "ORDERS" /* ORDERS */) {
|
|
768
858
|
const body = msg.body?.[0];
|
|
769
859
|
if (!body?.orderId) return;
|
|
770
|
-
if (body.status === "REJECTED") {
|
|
860
|
+
if (body.status === "REJECTED" /* REJECTED */) {
|
|
771
861
|
done(new Error(`[dxtrade-api] Order rejected: ${body.statusDescription ?? "Unknown reason"}`));
|
|
772
|
-
} else if (body.status === "FILLED") {
|
|
862
|
+
} else if (body.status === "FILLED" /* FILLED */) {
|
|
773
863
|
done(null, body);
|
|
774
864
|
}
|
|
775
865
|
}
|
|
@@ -779,161 +869,236 @@ function createOrderListener(wsUrl, cookieStr, timeout = 3e4, debug = false) {
|
|
|
779
869
|
settled = true;
|
|
780
870
|
clearTimeout(timer);
|
|
781
871
|
ws.close();
|
|
782
|
-
|
|
872
|
+
checkWsRateLimit(error);
|
|
873
|
+
reject(new DxtradeError("ORDER_ERROR" /* ORDER_ERROR */, `WebSocket order listener error: ${error.message}`));
|
|
783
874
|
});
|
|
784
875
|
});
|
|
785
876
|
return { promise, ready };
|
|
786
877
|
}
|
|
787
|
-
|
|
788
|
-
ctx.ensureSession();
|
|
789
|
-
if (ctx.wsManager) {
|
|
790
|
-
return ctx.wsManager.waitFor("ORDERS" /* ORDERS */, timeout);
|
|
791
|
-
}
|
|
792
|
-
const wsUrl = endpoints.websocket(ctx.broker, ctx.atmosphereId);
|
|
793
|
-
const cookieStr = Cookies.serialize(ctx.cookies);
|
|
878
|
+
function createWsManagerOrderListener(ctx, timeout = 3e4) {
|
|
794
879
|
return new Promise((resolve, reject) => {
|
|
795
|
-
|
|
880
|
+
let settled = false;
|
|
796
881
|
const timer = setTimeout(() => {
|
|
797
|
-
|
|
798
|
-
|
|
882
|
+
if (settled) return;
|
|
883
|
+
settled = true;
|
|
884
|
+
cleanup();
|
|
885
|
+
reject(new Error("[dxtrade-api] Order update timed out"));
|
|
799
886
|
}, timeout);
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
if (typeof msg === "string") return;
|
|
804
|
-
if (msg.type === "ORDERS" /* ORDERS */) {
|
|
805
|
-
clearTimeout(timer);
|
|
806
|
-
ws.close();
|
|
807
|
-
resolve(msg.body);
|
|
808
|
-
}
|
|
809
|
-
});
|
|
810
|
-
ws.on("error", (error) => {
|
|
887
|
+
function done(err, result) {
|
|
888
|
+
if (settled) return;
|
|
889
|
+
settled = true;
|
|
811
890
|
clearTimeout(timer);
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
891
|
+
cleanup();
|
|
892
|
+
if (err) reject(err);
|
|
893
|
+
else resolve(result);
|
|
894
|
+
}
|
|
895
|
+
function onMessage(body) {
|
|
896
|
+
const messages = body;
|
|
897
|
+
const orderMsg = messages?.findLast?.(
|
|
898
|
+
(m) => m.messageCategory === "TRADE_LOG" /* TRADE_LOG */ && m.messageType === "ORDER" /* ORDER */ && !m.historyMessage
|
|
899
|
+
);
|
|
900
|
+
if (!orderMsg) return;
|
|
901
|
+
const params = orderMsg.parametersTO;
|
|
902
|
+
if (params.orderStatus === "REJECTED" /* REJECTED */) {
|
|
903
|
+
const reason = params.rejectReason?.key ?? "Unknown reason";
|
|
904
|
+
done(new Error(`[dxtrade-api] Order rejected: ${reason}`));
|
|
905
|
+
} else if (params.orderStatus === "FILLED" /* FILLED */) {
|
|
906
|
+
done(null, {
|
|
907
|
+
orderId: params.orderKey,
|
|
908
|
+
status: params.orderStatus,
|
|
909
|
+
symbol: params.symbol,
|
|
910
|
+
filledQuantity: params.filledQuantity,
|
|
911
|
+
filledPrice: params.filledPrice,
|
|
912
|
+
positionCode: params.positionCode
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function onOrders(body) {
|
|
917
|
+
const order = body?.[0];
|
|
918
|
+
if (!order?.orderId) return;
|
|
919
|
+
if (order.status === "REJECTED" /* REJECTED */) {
|
|
920
|
+
done(new Error(`[dxtrade-api] Order rejected: ${order.statusDescription ?? "Unknown reason"}`));
|
|
921
|
+
} else if (order.status === "FILLED" /* FILLED */) {
|
|
922
|
+
done(null, order);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function cleanup() {
|
|
926
|
+
ctx.wsManager?.removeListener("MESSAGE" /* MESSAGE */, onMessage);
|
|
927
|
+
ctx.wsManager?.removeListener("ORDERS" /* ORDERS */, onOrders);
|
|
928
|
+
}
|
|
929
|
+
ctx.wsManager.on("MESSAGE" /* MESSAGE */, onMessage);
|
|
930
|
+
ctx.wsManager.on("ORDERS" /* ORDERS */, onOrders);
|
|
815
931
|
});
|
|
816
932
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
if (!accountId) {
|
|
821
|
-
ctx.throwError("CANCEL_ORDER_ERROR" /* CANCEL_ORDER_ERROR */, "accountId is required to cancel an order");
|
|
822
|
-
}
|
|
823
|
-
try {
|
|
824
|
-
await retryRequest(
|
|
825
|
-
{
|
|
826
|
-
method: "DELETE",
|
|
827
|
-
url: endpoints.cancelOrder(ctx.broker, accountId, orderChainId),
|
|
828
|
-
headers: authHeaders(ctx.csrf, Cookies.serialize(ctx.cookies))
|
|
829
|
-
},
|
|
830
|
-
ctx.retries
|
|
831
|
-
);
|
|
832
|
-
} catch (error) {
|
|
833
|
-
if (error instanceof DxtradeError) throw error;
|
|
834
|
-
const message = error instanceof Error ? error.response?.data?.message ?? error.message : "Unknown error";
|
|
835
|
-
ctx.throwError("CANCEL_ORDER_ERROR" /* CANCEL_ORDER_ERROR */, `Cancel order error: ${message}`);
|
|
933
|
+
var OrdersDomain = class {
|
|
934
|
+
constructor(_ctx) {
|
|
935
|
+
this._ctx = _ctx;
|
|
836
936
|
}
|
|
837
|
-
|
|
838
|
-
async
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
937
|
+
/** Get all pending/open orders via WebSocket. */
|
|
938
|
+
async get(timeout = 3e4) {
|
|
939
|
+
this._ctx.ensureSession();
|
|
940
|
+
if (this._ctx.wsManager) {
|
|
941
|
+
return this._ctx.wsManager.waitFor("ORDERS" /* ORDERS */, timeout);
|
|
942
|
+
}
|
|
943
|
+
const wsUrl = endpoints.websocket(this._ctx.broker, this._ctx.atmosphereId);
|
|
944
|
+
const cookieStr = Cookies.serialize(this._ctx.cookies);
|
|
945
|
+
return new Promise((resolve, reject) => {
|
|
946
|
+
const ws = new WebSocket6(wsUrl, { headers: { Cookie: cookieStr } });
|
|
947
|
+
const timer = setTimeout(() => {
|
|
948
|
+
ws.close();
|
|
949
|
+
reject(new DxtradeError("ORDERS_TIMEOUT" /* ORDERS_TIMEOUT */, "Orders request timed out"));
|
|
950
|
+
}, timeout);
|
|
951
|
+
ws.on("message", (data) => {
|
|
952
|
+
const msg = parseWsData(data);
|
|
953
|
+
if (shouldLog(msg, this._ctx.debug)) debugLog(msg);
|
|
954
|
+
if (typeof msg === "string") return;
|
|
955
|
+
if (msg.type === "ORDERS" /* ORDERS */) {
|
|
956
|
+
clearTimeout(timer);
|
|
957
|
+
ws.close();
|
|
958
|
+
resolve(msg.body);
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
ws.on("error", (error) => {
|
|
962
|
+
clearTimeout(timer);
|
|
963
|
+
ws.close();
|
|
964
|
+
checkWsRateLimit(error);
|
|
965
|
+
reject(new DxtradeError("ORDERS_ERROR" /* ORDERS_ERROR */, `Orders error: ${error.message}`));
|
|
966
|
+
});
|
|
967
|
+
});
|
|
843
968
|
}
|
|
844
|
-
|
|
845
|
-
async
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
const qty = side === "BUY" /* BUY */ ? units : -units;
|
|
866
|
-
const priceParam = orderType === "STOP" /* STOP */ ? "stopPrice" : "limitPrice";
|
|
867
|
-
const orderData = {
|
|
868
|
-
directExchange: false,
|
|
869
|
-
legs: [
|
|
870
|
-
{
|
|
871
|
-
...instrumentId != null && { instrumentId },
|
|
872
|
-
...positionCode != null && { positionCode },
|
|
873
|
-
positionEffect,
|
|
874
|
-
ratioQuantity: 1,
|
|
875
|
-
symbol
|
|
876
|
-
}
|
|
877
|
-
],
|
|
878
|
-
orderSide: side,
|
|
879
|
-
orderType,
|
|
880
|
-
quantity: qty,
|
|
881
|
-
requestId: orderCode ?? `gwt-uid-931-${crypto.randomUUID()}`,
|
|
882
|
-
timeInForce: tif,
|
|
883
|
-
...expireDate != null && { expireDate },
|
|
884
|
-
...metadata != null && { metadata }
|
|
885
|
-
};
|
|
886
|
-
if (price != null && orderType !== "MARKET" /* MARKET */) {
|
|
887
|
-
orderData[priceParam] = price;
|
|
969
|
+
/** Cancel a single pending order by its order chain ID. */
|
|
970
|
+
async cancel(orderChainId) {
|
|
971
|
+
this._ctx.ensureSession();
|
|
972
|
+
const accountId = this._ctx.accountId ?? this._ctx.config.accountId;
|
|
973
|
+
if (!accountId) {
|
|
974
|
+
this._ctx.throwError("CANCEL_ORDER_ERROR" /* CANCEL_ORDER_ERROR */, "accountId is required to cancel an order");
|
|
975
|
+
}
|
|
976
|
+
try {
|
|
977
|
+
await retryRequest(
|
|
978
|
+
{
|
|
979
|
+
method: "DELETE",
|
|
980
|
+
url: endpoints.cancelOrder(this._ctx.broker, accountId, orderChainId),
|
|
981
|
+
headers: authHeaders(this._ctx.csrf, Cookies.serialize(this._ctx.cookies))
|
|
982
|
+
},
|
|
983
|
+
this._ctx.retries
|
|
984
|
+
);
|
|
985
|
+
} catch (error) {
|
|
986
|
+
if (error instanceof DxtradeError) throw error;
|
|
987
|
+
const message = error instanceof Error ? error.response?.data?.message ?? error.message : "Unknown error";
|
|
988
|
+
this._ctx.throwError("CANCEL_ORDER_ERROR" /* CANCEL_ORDER_ERROR */, `Cancel order error: ${message}`);
|
|
989
|
+
}
|
|
888
990
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
orderType: "STOP" /* STOP */,
|
|
897
|
-
quantityForProtection: qty,
|
|
898
|
-
removed: false
|
|
899
|
-
};
|
|
991
|
+
/** Cancel all pending orders. */
|
|
992
|
+
async cancelAll() {
|
|
993
|
+
const orders = await this.get();
|
|
994
|
+
const pending = orders.filter((o) => !o.finalStatus);
|
|
995
|
+
for (const order of pending) {
|
|
996
|
+
await this.cancel(order.orderId);
|
|
997
|
+
}
|
|
900
998
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
999
|
+
/**
|
|
1000
|
+
* Submit a trading order and wait for WebSocket confirmation.
|
|
1001
|
+
* Supports market, limit, and stop orders with optional stop loss and take profit.
|
|
1002
|
+
*/
|
|
1003
|
+
async submit(params) {
|
|
1004
|
+
this._ctx.ensureSession();
|
|
1005
|
+
const {
|
|
1006
|
+
symbol,
|
|
1007
|
+
side,
|
|
1008
|
+
quantity,
|
|
1009
|
+
orderType,
|
|
1010
|
+
orderCode,
|
|
1011
|
+
price,
|
|
1012
|
+
instrumentId,
|
|
1013
|
+
stopLoss,
|
|
1014
|
+
takeProfit,
|
|
1015
|
+
positionEffect = "OPENING" /* OPENING */,
|
|
1016
|
+
positionCode,
|
|
1017
|
+
tif = "GTC",
|
|
1018
|
+
expireDate,
|
|
1019
|
+
metadata
|
|
1020
|
+
} = params;
|
|
1021
|
+
const info = await new SymbolsDomain(this._ctx).info(symbol);
|
|
1022
|
+
const units = quantity * info.lotSize;
|
|
1023
|
+
const qty = side === "BUY" /* BUY */ ? units : -units;
|
|
1024
|
+
const priceParam = orderType === "STOP" /* STOP */ ? "stopPrice" : "limitPrice";
|
|
1025
|
+
const orderData = {
|
|
1026
|
+
directExchange: false,
|
|
1027
|
+
legs: [
|
|
1028
|
+
{
|
|
1029
|
+
...instrumentId != null && { instrumentId },
|
|
1030
|
+
...positionCode != null && { positionCode },
|
|
1031
|
+
positionEffect,
|
|
1032
|
+
ratioQuantity: 1,
|
|
1033
|
+
symbol
|
|
1034
|
+
}
|
|
1035
|
+
],
|
|
1036
|
+
orderSide: side,
|
|
1037
|
+
orderType,
|
|
1038
|
+
quantity: qty,
|
|
1039
|
+
requestId: orderCode ?? `gwt-uid-931-${crypto.randomUUID()}`,
|
|
1040
|
+
timeInForce: tif,
|
|
1041
|
+
...expireDate != null && { expireDate },
|
|
1042
|
+
...metadata != null && { metadata }
|
|
911
1043
|
};
|
|
1044
|
+
if (price != null) {
|
|
1045
|
+
orderData[priceParam] = price;
|
|
1046
|
+
}
|
|
1047
|
+
if (stopLoss) {
|
|
1048
|
+
orderData.stopLoss = {
|
|
1049
|
+
...stopLoss.offset != null && { fixedOffset: stopLoss.offset },
|
|
1050
|
+
...stopLoss.price != null && { fixedPrice: stopLoss.price },
|
|
1051
|
+
priceFixed: stopLoss.price != null,
|
|
1052
|
+
orderChainId: 0,
|
|
1053
|
+
orderId: 0,
|
|
1054
|
+
orderType: "STOP" /* STOP */,
|
|
1055
|
+
quantityForProtection: qty,
|
|
1056
|
+
removed: false
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
if (takeProfit) {
|
|
1060
|
+
orderData.takeProfit = {
|
|
1061
|
+
...takeProfit.offset != null && { fixedOffset: takeProfit.offset },
|
|
1062
|
+
...takeProfit.price != null && { fixedPrice: takeProfit.price },
|
|
1063
|
+
priceFixed: takeProfit.price != null,
|
|
1064
|
+
orderChainId: 0,
|
|
1065
|
+
orderId: 0,
|
|
1066
|
+
orderType: "LIMIT" /* LIMIT */,
|
|
1067
|
+
quantityForProtection: qty,
|
|
1068
|
+
removed: false
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
try {
|
|
1072
|
+
let listenerPromise;
|
|
1073
|
+
if (this._ctx.wsManager) {
|
|
1074
|
+
listenerPromise = createWsManagerOrderListener(this._ctx, 3e4);
|
|
1075
|
+
} else {
|
|
1076
|
+
const wsUrl = endpoints.websocket(this._ctx.broker, this._ctx.atmosphereId);
|
|
1077
|
+
const cookieStr = Cookies.serialize(this._ctx.cookies);
|
|
1078
|
+
const listener = createOrderListener(wsUrl, cookieStr, 3e4, this._ctx.debug);
|
|
1079
|
+
await listener.ready;
|
|
1080
|
+
listenerPromise = listener.promise;
|
|
1081
|
+
}
|
|
1082
|
+
const response = await retryRequest(
|
|
1083
|
+
{
|
|
1084
|
+
method: "POST",
|
|
1085
|
+
url: endpoints.submitOrder(this._ctx.broker),
|
|
1086
|
+
data: orderData,
|
|
1087
|
+
headers: authHeaders(this._ctx.csrf, Cookies.serialize(this._ctx.cookies))
|
|
1088
|
+
},
|
|
1089
|
+
this._ctx.retries
|
|
1090
|
+
);
|
|
1091
|
+
this._ctx.callbacks.onOrderPlaced?.(response.data);
|
|
1092
|
+
const orderUpdate = await listenerPromise;
|
|
1093
|
+
this._ctx.callbacks.onOrderUpdate?.(orderUpdate);
|
|
1094
|
+
return orderUpdate;
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
if (error instanceof DxtradeError) throw error;
|
|
1097
|
+
const message = error instanceof Error ? error.response?.data?.message ?? error.message : "Unknown error";
|
|
1098
|
+
this._ctx.throwError("ORDER_ERROR" /* ORDER_ERROR */, `Error submitting order: ${message}`);
|
|
1099
|
+
}
|
|
912
1100
|
}
|
|
913
|
-
|
|
914
|
-
const wsUrl = endpoints.websocket(ctx.broker, ctx.atmosphereId);
|
|
915
|
-
const cookieStr = Cookies.serialize(ctx.cookies);
|
|
916
|
-
const listener = createOrderListener(wsUrl, cookieStr, 3e4, ctx.debug);
|
|
917
|
-
await listener.ready;
|
|
918
|
-
const response = await retryRequest(
|
|
919
|
-
{
|
|
920
|
-
method: "POST",
|
|
921
|
-
url: endpoints.submitOrder(ctx.broker),
|
|
922
|
-
data: orderData,
|
|
923
|
-
headers: authHeaders(ctx.csrf, Cookies.serialize(ctx.cookies))
|
|
924
|
-
},
|
|
925
|
-
ctx.retries
|
|
926
|
-
);
|
|
927
|
-
ctx.callbacks.onOrderPlaced?.(response.data);
|
|
928
|
-
const orderUpdate = await listener.promise;
|
|
929
|
-
ctx.callbacks.onOrderUpdate?.(orderUpdate);
|
|
930
|
-
return orderUpdate;
|
|
931
|
-
} catch (error) {
|
|
932
|
-
if (error instanceof DxtradeError) throw error;
|
|
933
|
-
const message = error instanceof Error ? error.response?.data?.message ?? error.message : "Unknown error";
|
|
934
|
-
ctx.throwError("ORDER_ERROR" /* ORDER_ERROR */, `Error submitting order: ${message}`);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
1101
|
+
};
|
|
937
1102
|
|
|
938
1103
|
// src/domains/position/position.ts
|
|
939
1104
|
import WebSocket7 from "ws";
|
|
@@ -954,110 +1119,239 @@ function mergePositionsWithMetrics(positions, metrics) {
|
|
|
954
1119
|
};
|
|
955
1120
|
});
|
|
956
1121
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
"STREAM_REQUIRES_CONNECT" /* STREAM_REQUIRES_CONNECT */,
|
|
961
|
-
"Streaming requires a persistent WebSocket. Use connect() instead of auth()."
|
|
962
|
-
);
|
|
1122
|
+
var PositionsDomain = class {
|
|
1123
|
+
constructor(_ctx) {
|
|
1124
|
+
this._ctx = _ctx;
|
|
963
1125
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1126
|
+
/** Stream real-time position updates with P&L metrics. Requires connect(). Returns unsubscribe function. */
|
|
1127
|
+
stream(callback) {
|
|
1128
|
+
if (!this._ctx.wsManager) {
|
|
1129
|
+
this._ctx.throwError(
|
|
1130
|
+
"STREAM_REQUIRES_CONNECT" /* STREAM_REQUIRES_CONNECT */,
|
|
1131
|
+
"Streaming requires a persistent WebSocket. Use connect() instead of auth()."
|
|
1132
|
+
);
|
|
969
1133
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
ctx.wsManager.on("POSITIONS" /* POSITIONS */, onPositions);
|
|
974
|
-
ctx.wsManager.on("POSITION_METRICS" /* POSITION_METRICS */, onMetrics);
|
|
975
|
-
emit();
|
|
976
|
-
return () => {
|
|
977
|
-
ctx.wsManager?.removeListener("POSITIONS" /* POSITIONS */, onPositions);
|
|
978
|
-
ctx.wsManager?.removeListener("POSITION_METRICS" /* POSITION_METRICS */, onMetrics);
|
|
979
|
-
};
|
|
980
|
-
}
|
|
981
|
-
async function getPositions(ctx) {
|
|
982
|
-
ctx.ensureSession();
|
|
983
|
-
if (ctx.wsManager) {
|
|
984
|
-
const [positions, metrics] = await Promise.all([
|
|
985
|
-
ctx.wsManager.waitFor("POSITIONS" /* POSITIONS */),
|
|
986
|
-
ctx.wsManager.waitFor("POSITION_METRICS" /* POSITION_METRICS */)
|
|
987
|
-
]);
|
|
988
|
-
return mergePositionsWithMetrics(positions, metrics);
|
|
989
|
-
}
|
|
990
|
-
const wsUrl = endpoints.websocket(ctx.broker, ctx.atmosphereId);
|
|
991
|
-
const cookieStr = Cookies.serialize(ctx.cookies);
|
|
992
|
-
return new Promise((resolve, reject) => {
|
|
993
|
-
const ws = new WebSocket7(wsUrl, { headers: { Cookie: cookieStr } });
|
|
994
|
-
let positions = null;
|
|
995
|
-
let metrics = null;
|
|
996
|
-
const timer = setTimeout(() => {
|
|
997
|
-
ws.close();
|
|
998
|
-
reject(new DxtradeError("ACCOUNT_POSITIONS_TIMEOUT" /* ACCOUNT_POSITIONS_TIMEOUT */, "Account positions timed out"));
|
|
999
|
-
}, 3e4);
|
|
1000
|
-
ws.on("message", (data) => {
|
|
1001
|
-
const msg = parseWsData(data);
|
|
1002
|
-
if (shouldLog(msg, ctx.debug)) debugLog(msg);
|
|
1003
|
-
if (typeof msg === "string") return;
|
|
1004
|
-
if (msg.type === "POSITIONS" /* POSITIONS */) {
|
|
1005
|
-
positions = msg.body;
|
|
1006
|
-
}
|
|
1007
|
-
if (msg.type === "POSITION_METRICS" /* POSITION_METRICS */) {
|
|
1008
|
-
metrics = msg.body;
|
|
1009
|
-
}
|
|
1134
|
+
const emit = () => {
|
|
1135
|
+
const positions = this._ctx.wsManager.getCached("POSITIONS" /* POSITIONS */);
|
|
1136
|
+
const metrics = this._ctx.wsManager.getCached("POSITION_METRICS" /* POSITION_METRICS */);
|
|
1010
1137
|
if (positions && metrics) {
|
|
1138
|
+
callback(mergePositionsWithMetrics(positions, metrics));
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
const onPositions = () => emit();
|
|
1142
|
+
const onMetrics = () => emit();
|
|
1143
|
+
this._ctx.wsManager.on("POSITIONS" /* POSITIONS */, onPositions);
|
|
1144
|
+
this._ctx.wsManager.on("POSITION_METRICS" /* POSITION_METRICS */, onMetrics);
|
|
1145
|
+
emit();
|
|
1146
|
+
return () => {
|
|
1147
|
+
this._ctx.wsManager?.removeListener("POSITIONS" /* POSITIONS */, onPositions);
|
|
1148
|
+
this._ctx.wsManager?.removeListener("POSITION_METRICS" /* POSITION_METRICS */, onMetrics);
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
/** Get all open positions with P&L metrics merged. */
|
|
1152
|
+
async get() {
|
|
1153
|
+
this._ctx.ensureSession();
|
|
1154
|
+
if (this._ctx.wsManager) {
|
|
1155
|
+
const [positions, metrics] = await Promise.all([
|
|
1156
|
+
this._ctx.wsManager.waitFor("POSITIONS" /* POSITIONS */),
|
|
1157
|
+
this._ctx.wsManager.waitFor("POSITION_METRICS" /* POSITION_METRICS */)
|
|
1158
|
+
]);
|
|
1159
|
+
return mergePositionsWithMetrics(positions, metrics);
|
|
1160
|
+
}
|
|
1161
|
+
const wsUrl = endpoints.websocket(this._ctx.broker, this._ctx.atmosphereId);
|
|
1162
|
+
const cookieStr = Cookies.serialize(this._ctx.cookies);
|
|
1163
|
+
return new Promise((resolve, reject) => {
|
|
1164
|
+
const ws = new WebSocket7(wsUrl, { headers: { Cookie: cookieStr } });
|
|
1165
|
+
let positions = null;
|
|
1166
|
+
let metrics = null;
|
|
1167
|
+
const timer = setTimeout(() => {
|
|
1168
|
+
ws.close();
|
|
1169
|
+
reject(new DxtradeError("ACCOUNT_POSITIONS_TIMEOUT" /* ACCOUNT_POSITIONS_TIMEOUT */, "Account positions timed out"));
|
|
1170
|
+
}, 3e4);
|
|
1171
|
+
ws.on("message", (data) => {
|
|
1172
|
+
const msg = parseWsData(data);
|
|
1173
|
+
if (shouldLog(msg, this._ctx.debug)) debugLog(msg);
|
|
1174
|
+
if (typeof msg === "string") return;
|
|
1175
|
+
if (msg.type === "POSITIONS" /* POSITIONS */) {
|
|
1176
|
+
positions = msg.body;
|
|
1177
|
+
}
|
|
1178
|
+
if (msg.type === "POSITION_METRICS" /* POSITION_METRICS */) {
|
|
1179
|
+
metrics = msg.body;
|
|
1180
|
+
}
|
|
1181
|
+
if (positions && metrics) {
|
|
1182
|
+
clearTimeout(timer);
|
|
1183
|
+
ws.close();
|
|
1184
|
+
resolve(mergePositionsWithMetrics(positions, metrics));
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
ws.on("error", (error) => {
|
|
1011
1188
|
clearTimeout(timer);
|
|
1012
1189
|
ws.close();
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
ws.on("error", (error) => {
|
|
1017
|
-
clearTimeout(timer);
|
|
1018
|
-
ws.close();
|
|
1019
|
-
reject(new DxtradeError("ACCOUNT_POSITIONS_ERROR" /* ACCOUNT_POSITIONS_ERROR */, `Account positions error: ${error.message}`));
|
|
1190
|
+
checkWsRateLimit(error);
|
|
1191
|
+
reject(new DxtradeError("ACCOUNT_POSITIONS_ERROR" /* ACCOUNT_POSITIONS_ERROR */, `Account positions error: ${error.message}`));
|
|
1192
|
+
});
|
|
1020
1193
|
});
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
async
|
|
1024
|
-
|
|
1025
|
-
|
|
1194
|
+
}
|
|
1195
|
+
/** Close all open positions with market orders. */
|
|
1196
|
+
async closeAll() {
|
|
1197
|
+
const positions = await this.get();
|
|
1198
|
+
for (const pos of positions) {
|
|
1199
|
+
const closeData = {
|
|
1200
|
+
legs: [
|
|
1201
|
+
{
|
|
1202
|
+
instrumentId: pos.positionKey.instrumentId,
|
|
1203
|
+
positionCode: pos.positionKey.positionCode,
|
|
1204
|
+
positionEffect: "CLOSING",
|
|
1205
|
+
ratioQuantity: 1,
|
|
1206
|
+
symbol: pos.positionKey.positionCode
|
|
1207
|
+
}
|
|
1208
|
+
],
|
|
1209
|
+
limitPrice: 0,
|
|
1210
|
+
orderType: "MARKET",
|
|
1211
|
+
quantity: -pos.quantity,
|
|
1212
|
+
timeInForce: "GTC"
|
|
1213
|
+
};
|
|
1214
|
+
await this._sendCloseRequest(closeData);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
/** Close a position by its position code. Returns the position with P&L metrics. Optionally wait for close confirmation via `waitForClose: "stream" | "poll"`. */
|
|
1218
|
+
async close(positionCode, options) {
|
|
1219
|
+
const positions = await this.get();
|
|
1220
|
+
const position = positions.find((p) => p.positionKey.positionCode === positionCode);
|
|
1221
|
+
if (!position) {
|
|
1222
|
+
this._ctx.throwError("POSITION_NOT_FOUND" /* POSITION_NOT_FOUND */, `Position with code "${positionCode}" not found`);
|
|
1223
|
+
}
|
|
1026
1224
|
const closeData = {
|
|
1027
1225
|
legs: [
|
|
1028
1226
|
{
|
|
1029
|
-
instrumentId:
|
|
1030
|
-
positionCode:
|
|
1227
|
+
instrumentId: position.positionKey.instrumentId,
|
|
1228
|
+
positionCode: position.positionKey.positionCode,
|
|
1031
1229
|
positionEffect: "CLOSING",
|
|
1032
1230
|
ratioQuantity: 1,
|
|
1033
|
-
symbol:
|
|
1231
|
+
symbol: position.positionKey.positionCode
|
|
1034
1232
|
}
|
|
1035
1233
|
],
|
|
1036
1234
|
limitPrice: 0,
|
|
1037
1235
|
orderType: "MARKET",
|
|
1038
|
-
quantity: -
|
|
1236
|
+
quantity: -position.quantity,
|
|
1039
1237
|
timeInForce: "GTC"
|
|
1040
1238
|
};
|
|
1041
|
-
|
|
1239
|
+
if (options?.waitForClose === "stream") {
|
|
1240
|
+
return this._waitForCloseStream(positionCode, position, closeData, options.timeout ?? 3e4);
|
|
1241
|
+
}
|
|
1242
|
+
await this._sendCloseRequest(closeData);
|
|
1243
|
+
if (options?.waitForClose === "poll") {
|
|
1244
|
+
return this._waitForClosePoll(positionCode, position, options.timeout ?? 3e4, options.pollInterval ?? 1e3);
|
|
1245
|
+
}
|
|
1246
|
+
return position;
|
|
1247
|
+
}
|
|
1248
|
+
_waitForCloseStream(positionCode, lastSnapshot, closeData, timeout) {
|
|
1249
|
+
if (!this._ctx.wsManager) {
|
|
1250
|
+
this._ctx.throwError(
|
|
1251
|
+
"STREAM_REQUIRES_CONNECT" /* STREAM_REQUIRES_CONNECT */,
|
|
1252
|
+
'waitForClose: "stream" requires a persistent WebSocket. Use connect() instead of auth(), or use "poll" mode.'
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
return new Promise(async (resolve, reject) => {
|
|
1256
|
+
let settled = false;
|
|
1257
|
+
const result = lastSnapshot;
|
|
1258
|
+
const timer = setTimeout(() => {
|
|
1259
|
+
if (settled) return;
|
|
1260
|
+
settled = true;
|
|
1261
|
+
cleanup();
|
|
1262
|
+
reject(
|
|
1263
|
+
new DxtradeError("POSITION_CLOSE_TIMEOUT" /* POSITION_CLOSE_TIMEOUT */, `Position close confirmation timed out after ${timeout}ms`)
|
|
1264
|
+
);
|
|
1265
|
+
}, timeout);
|
|
1266
|
+
function done(err, res) {
|
|
1267
|
+
if (settled) return;
|
|
1268
|
+
settled = true;
|
|
1269
|
+
clearTimeout(timer);
|
|
1270
|
+
cleanup();
|
|
1271
|
+
if (err) reject(err);
|
|
1272
|
+
else resolve(res);
|
|
1273
|
+
}
|
|
1274
|
+
function onMessage(body) {
|
|
1275
|
+
const messages = body;
|
|
1276
|
+
const orderMsg = messages?.findLast?.(
|
|
1277
|
+
(m) => m.messageCategory === "TRADE_LOG" /* TRADE_LOG */ && m.messageType === "ORDER" /* ORDER */ && !m.historyMessage
|
|
1278
|
+
);
|
|
1279
|
+
if (!orderMsg) return;
|
|
1280
|
+
const params = orderMsg.parametersTO;
|
|
1281
|
+
if (params.positionCode !== positionCode) return;
|
|
1282
|
+
if (params.orderStatus === "REJECTED" /* REJECTED */) {
|
|
1283
|
+
done(
|
|
1284
|
+
new DxtradeError(
|
|
1285
|
+
"POSITION_CLOSE_ERROR" /* POSITION_CLOSE_ERROR */,
|
|
1286
|
+
`Close order rejected: ${params.rejectReason?.key ?? "Unknown reason"}`
|
|
1287
|
+
)
|
|
1288
|
+
);
|
|
1289
|
+
} else if (params.orderStatus === "FILLED" /* FILLED */) {
|
|
1290
|
+
done(null, result);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
function onOrders(body) {
|
|
1294
|
+
const orders = body;
|
|
1295
|
+
const order = orders?.[0];
|
|
1296
|
+
if (!order?.orderId) return;
|
|
1297
|
+
if (order.status === "REJECTED" /* REJECTED */) {
|
|
1298
|
+
done(
|
|
1299
|
+
new DxtradeError(
|
|
1300
|
+
"POSITION_CLOSE_ERROR" /* POSITION_CLOSE_ERROR */,
|
|
1301
|
+
`Close order rejected: ${order.statusDescription ?? "Unknown reason"}`
|
|
1302
|
+
)
|
|
1303
|
+
);
|
|
1304
|
+
} else if (order.status === "FILLED" /* FILLED */) {
|
|
1305
|
+
done(null, result);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
const wsManager = this._ctx.wsManager;
|
|
1309
|
+
function cleanup() {
|
|
1310
|
+
wsManager.removeListener("MESSAGE" /* MESSAGE */, onMessage);
|
|
1311
|
+
wsManager.removeListener("ORDERS" /* ORDERS */, onOrders);
|
|
1312
|
+
}
|
|
1313
|
+
wsManager.on("MESSAGE" /* MESSAGE */, onMessage);
|
|
1314
|
+
wsManager.on("ORDERS" /* ORDERS */, onOrders);
|
|
1315
|
+
try {
|
|
1316
|
+
await this._sendCloseRequest(closeData);
|
|
1317
|
+
} catch (error) {
|
|
1318
|
+
done(error instanceof Error ? error : new Error(String(error)));
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1042
1321
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
const message = error instanceof Error ? error.response?.data?.message ?? error.message : "Unknown error";
|
|
1058
|
-
ctx.throwError("POSITION_CLOSE_ERROR" /* POSITION_CLOSE_ERROR */, `Position close error: ${message}`);
|
|
1322
|
+
async _waitForClosePoll(positionCode, lastSnapshot, timeout, interval) {
|
|
1323
|
+
const deadline = Date.now() + timeout;
|
|
1324
|
+
let result = lastSnapshot;
|
|
1325
|
+
while (Date.now() < deadline) {
|
|
1326
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
1327
|
+
const positions = await this.get();
|
|
1328
|
+
const match = positions.find((p) => p.positionKey.positionCode === positionCode);
|
|
1329
|
+
if (match) {
|
|
1330
|
+
result = match;
|
|
1331
|
+
} else {
|
|
1332
|
+
return result;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
this._ctx.throwError("POSITION_CLOSE_TIMEOUT" /* POSITION_CLOSE_TIMEOUT */, `Position close confirmation timed out after ${timeout}ms`);
|
|
1059
1336
|
}
|
|
1060
|
-
|
|
1337
|
+
async _sendCloseRequest(data) {
|
|
1338
|
+
try {
|
|
1339
|
+
await retryRequest(
|
|
1340
|
+
{
|
|
1341
|
+
method: "POST",
|
|
1342
|
+
url: endpoints.closePosition(this._ctx.broker),
|
|
1343
|
+
data,
|
|
1344
|
+
headers: authHeaders(this._ctx.csrf, Cookies.serialize(this._ctx.cookies))
|
|
1345
|
+
},
|
|
1346
|
+
this._ctx.retries
|
|
1347
|
+
);
|
|
1348
|
+
} catch (error) {
|
|
1349
|
+
if (error instanceof DxtradeError) throw error;
|
|
1350
|
+
const message = error instanceof Error ? error.response?.data?.message ?? error.message : "Unknown error";
|
|
1351
|
+
this._ctx.throwError("POSITION_CLOSE_ERROR" /* POSITION_CLOSE_ERROR */, `Position close error: ${message}`);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1061
1355
|
|
|
1062
1356
|
// src/domains/session/session.ts
|
|
1063
1357
|
import WebSocket8 from "ws";
|
|
@@ -1085,247 +1379,161 @@ function waitForHandshake(wsUrl, cookieStr, timeout = 3e4, debug = false) {
|
|
|
1085
1379
|
ws.on("error", (error) => {
|
|
1086
1380
|
clearTimeout(timer);
|
|
1087
1381
|
ws.close();
|
|
1382
|
+
checkWsRateLimit(error);
|
|
1088
1383
|
reject(new Error(`[dxtrade-api] WebSocket handshake error: ${error.message}`));
|
|
1089
1384
|
});
|
|
1090
1385
|
});
|
|
1091
1386
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1387
|
+
var SessionDomain = class {
|
|
1388
|
+
constructor(_ctx) {
|
|
1389
|
+
this._ctx = _ctx;
|
|
1390
|
+
}
|
|
1391
|
+
/** Authenticate with the broker using username and password. */
|
|
1392
|
+
async login() {
|
|
1393
|
+
try {
|
|
1394
|
+
const response = await retryRequest(
|
|
1395
|
+
{
|
|
1396
|
+
method: "POST",
|
|
1397
|
+
url: endpoints.login(this._ctx.broker),
|
|
1398
|
+
data: {
|
|
1399
|
+
username: this._ctx.config.username,
|
|
1400
|
+
password: this._ctx.config.password,
|
|
1401
|
+
// TODO:: take a look at this below, domain nor vendor seems required. it works if i comment out both.
|
|
1402
|
+
// however i still use it since i see brokers use it as well in the login endpoint.
|
|
1403
|
+
// domain: this._ctx.config.broker,
|
|
1404
|
+
vendor: this._ctx.config.broker
|
|
1405
|
+
// END TODO::
|
|
1406
|
+
},
|
|
1407
|
+
headers: {
|
|
1408
|
+
...baseHeaders(),
|
|
1409
|
+
Origin: this._ctx.broker,
|
|
1410
|
+
Referer: this._ctx.broker + "/",
|
|
1411
|
+
Cookie: Cookies.serialize(this._ctx.cookies)
|
|
1412
|
+
}
|
|
1106
1413
|
},
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1414
|
+
this._ctx.retries
|
|
1415
|
+
);
|
|
1416
|
+
if (response.status === 200) {
|
|
1417
|
+
const setCookies = response.headers["set-cookie"] ?? [];
|
|
1418
|
+
const incoming = Cookies.parse(setCookies);
|
|
1419
|
+
this._ctx.cookies = Cookies.merge(this._ctx.cookies, incoming);
|
|
1420
|
+
this._ctx.callbacks.onLogin?.();
|
|
1421
|
+
} else {
|
|
1422
|
+
this._ctx.throwError("LOGIN_FAILED" /* LOGIN_FAILED */, `Login failed: ${response.status}`);
|
|
1423
|
+
}
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
if (error instanceof DxtradeError) throw error;
|
|
1426
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1427
|
+
this._ctx.throwError("LOGIN_ERROR" /* LOGIN_ERROR */, `Login error: ${message}`);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
/** Fetch the CSRF token required for authenticated requests. */
|
|
1431
|
+
async fetchCsrf() {
|
|
1432
|
+
try {
|
|
1433
|
+
const cookieStr = Cookies.serialize(this._ctx.cookies);
|
|
1434
|
+
const response = await retryRequest(
|
|
1435
|
+
{
|
|
1436
|
+
method: "GET",
|
|
1437
|
+
url: this._ctx.broker,
|
|
1438
|
+
headers: { ...cookieOnlyHeaders(cookieStr), Referer: this._ctx.broker }
|
|
1439
|
+
},
|
|
1440
|
+
this._ctx.retries
|
|
1441
|
+
);
|
|
1112
1442
|
const setCookies = response.headers["set-cookie"] ?? [];
|
|
1113
1443
|
const incoming = Cookies.parse(setCookies);
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1444
|
+
this._ctx.cookies = Cookies.merge(this._ctx.cookies, incoming);
|
|
1445
|
+
const csrfMatch = response.data?.match(/name="csrf" content="([^"]+)"/);
|
|
1446
|
+
if (csrfMatch) {
|
|
1447
|
+
this._ctx.csrf = csrfMatch[1];
|
|
1448
|
+
} else {
|
|
1449
|
+
this._ctx.throwError("CSRF_NOT_FOUND" /* CSRF_NOT_FOUND */, "CSRF token not found");
|
|
1450
|
+
}
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
if (error instanceof DxtradeError) throw error;
|
|
1453
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1454
|
+
this._ctx.throwError("CSRF_ERROR" /* CSRF_ERROR */, `CSRF fetch error: ${message}`);
|
|
1118
1455
|
}
|
|
1119
|
-
} catch (error) {
|
|
1120
|
-
if (error instanceof DxtradeError) throw error;
|
|
1121
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1122
|
-
ctx.throwError("LOGIN_ERROR" /* LOGIN_ERROR */, `Login error: ${message}`);
|
|
1123
1456
|
}
|
|
1124
|
-
|
|
1125
|
-
async
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1457
|
+
/** Switch to a specific trading account by ID. */
|
|
1458
|
+
async switchAccount(accountId) {
|
|
1459
|
+
this._ctx.ensureSession();
|
|
1460
|
+
try {
|
|
1461
|
+
await retryRequest(
|
|
1462
|
+
{
|
|
1463
|
+
method: "POST",
|
|
1464
|
+
url: endpoints.switchAccount(this._ctx.broker, accountId),
|
|
1465
|
+
headers: authHeaders(this._ctx.csrf, Cookies.serialize(this._ctx.cookies))
|
|
1466
|
+
},
|
|
1467
|
+
this._ctx.retries
|
|
1468
|
+
);
|
|
1469
|
+
this._ctx.callbacks.onAccountSwitch?.(accountId);
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
if (error instanceof DxtradeError) throw error;
|
|
1472
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1473
|
+
this._ctx.throwError("ACCOUNT_SWITCH_ERROR" /* ACCOUNT_SWITCH_ERROR */, `Error switching account: ${message}`);
|
|
1141
1474
|
}
|
|
1142
|
-
} catch (error) {
|
|
1143
|
-
if (error instanceof DxtradeError) throw error;
|
|
1144
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1145
|
-
ctx.throwError("CSRF_ERROR" /* CSRF_ERROR */, `CSRF fetch error: ${message}`);
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
async function switchAccount(ctx, accountId) {
|
|
1149
|
-
ctx.ensureSession();
|
|
1150
|
-
try {
|
|
1151
|
-
await retryRequest(
|
|
1152
|
-
{
|
|
1153
|
-
method: "POST",
|
|
1154
|
-
url: endpoints.switchAccount(ctx.broker, accountId),
|
|
1155
|
-
headers: authHeaders(ctx.csrf, Cookies.serialize(ctx.cookies))
|
|
1156
|
-
},
|
|
1157
|
-
ctx.retries
|
|
1158
|
-
);
|
|
1159
|
-
ctx.callbacks.onAccountSwitch?.(accountId);
|
|
1160
|
-
} catch (error) {
|
|
1161
|
-
if (error instanceof DxtradeError) throw error;
|
|
1162
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1163
|
-
ctx.throwError("ACCOUNT_SWITCH_ERROR" /* ACCOUNT_SWITCH_ERROR */, `Error switching account: ${message}`);
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
async function auth(ctx) {
|
|
1167
|
-
await login(ctx);
|
|
1168
|
-
await fetchCsrf(ctx);
|
|
1169
|
-
if (ctx.debug) clearDebugLog();
|
|
1170
|
-
const cookieStr = Cookies.serialize(ctx.cookies);
|
|
1171
|
-
const handshake = await waitForHandshake(endpoints.websocket(ctx.broker), cookieStr, 3e4, ctx.debug);
|
|
1172
|
-
ctx.atmosphereId = handshake.atmosphereId;
|
|
1173
|
-
ctx.accountId = handshake.accountId;
|
|
1174
|
-
if (ctx.config.accountId) {
|
|
1175
|
-
await switchAccount(ctx, ctx.config.accountId);
|
|
1176
|
-
const reconnect = await waitForHandshake(
|
|
1177
|
-
endpoints.websocket(ctx.broker, ctx.atmosphereId),
|
|
1178
|
-
Cookies.serialize(ctx.cookies),
|
|
1179
|
-
3e4,
|
|
1180
|
-
ctx.debug
|
|
1181
|
-
);
|
|
1182
|
-
ctx.atmosphereId = reconnect.atmosphereId;
|
|
1183
|
-
ctx.accountId = reconnect.accountId;
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
async function connect(ctx) {
|
|
1187
|
-
await auth(ctx);
|
|
1188
|
-
const wsManager = new WsManager();
|
|
1189
|
-
const wsUrl = endpoints.websocket(ctx.broker, ctx.atmosphereId);
|
|
1190
|
-
const cookieStr = Cookies.serialize(ctx.cookies);
|
|
1191
|
-
await wsManager.connect(wsUrl, cookieStr, ctx.debug);
|
|
1192
|
-
ctx.wsManager = wsManager;
|
|
1193
|
-
}
|
|
1194
|
-
function disconnect(ctx) {
|
|
1195
|
-
if (ctx.wsManager) {
|
|
1196
|
-
ctx.wsManager.close();
|
|
1197
|
-
ctx.wsManager = null;
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
// src/client.ts
|
|
1202
|
-
var PositionsDomain = class {
|
|
1203
|
-
constructor(_ctx) {
|
|
1204
|
-
this._ctx = _ctx;
|
|
1205
|
-
}
|
|
1206
|
-
/** Get all open positions with P&L metrics merged. */
|
|
1207
|
-
get() {
|
|
1208
|
-
return getPositions(this._ctx);
|
|
1209
|
-
}
|
|
1210
|
-
/** Close a position. Supports partial closes by specifying a quantity smaller than the full position size. */
|
|
1211
|
-
close(params) {
|
|
1212
|
-
return closePosition(this._ctx, params);
|
|
1213
|
-
}
|
|
1214
|
-
/** Close all open positions with market orders. */
|
|
1215
|
-
closeAll() {
|
|
1216
|
-
return closeAllPositions(this._ctx);
|
|
1217
1475
|
}
|
|
1218
|
-
/**
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
*/
|
|
1235
|
-
submit(params) {
|
|
1236
|
-
return submitOrder(this._ctx, params);
|
|
1237
|
-
}
|
|
1238
|
-
/** Cancel a single pending order by its order chain ID. */
|
|
1239
|
-
cancel(orderChainId) {
|
|
1240
|
-
return cancelOrder(this._ctx, orderChainId);
|
|
1241
|
-
}
|
|
1242
|
-
/** Cancel all pending orders. */
|
|
1243
|
-
cancelAll() {
|
|
1244
|
-
return cancelAllOrders(this._ctx);
|
|
1245
|
-
}
|
|
1246
|
-
};
|
|
1247
|
-
var AccountDomain = class {
|
|
1248
|
-
constructor(_ctx) {
|
|
1249
|
-
this._ctx = _ctx;
|
|
1250
|
-
}
|
|
1251
|
-
/** Get account metrics including equity, balance, margin, and open P&L. */
|
|
1252
|
-
metrics() {
|
|
1253
|
-
return getAccountMetrics(this._ctx);
|
|
1254
|
-
}
|
|
1255
|
-
/**
|
|
1256
|
-
* Fetch trade journal entries for a date range.
|
|
1257
|
-
* @param params.from - Start timestamp (Unix ms)
|
|
1258
|
-
* @param params.to - End timestamp (Unix ms)
|
|
1259
|
-
*/
|
|
1260
|
-
tradeJournal(params) {
|
|
1261
|
-
return getTradeJournal(this._ctx, params);
|
|
1262
|
-
}
|
|
1263
|
-
/**
|
|
1264
|
-
* Fetch trade history for a date range.
|
|
1265
|
-
* @param params.from - Start timestamp (Unix ms)
|
|
1266
|
-
* @param params.to - End timestamp (Unix ms)
|
|
1267
|
-
*/
|
|
1268
|
-
tradeHistory(params) {
|
|
1269
|
-
return getTradeHistory(this._ctx, params);
|
|
1270
|
-
}
|
|
1271
|
-
};
|
|
1272
|
-
var SymbolsDomain = class {
|
|
1273
|
-
constructor(_ctx) {
|
|
1274
|
-
this._ctx = _ctx;
|
|
1275
|
-
}
|
|
1276
|
-
/** Search for symbols matching the given text (e.g. "EURUSD", "BTC"). */
|
|
1277
|
-
search(text) {
|
|
1278
|
-
return getSymbolSuggestions(this._ctx, text);
|
|
1279
|
-
}
|
|
1280
|
-
/** Get detailed instrument info for a symbol, including volume limits and lot size. */
|
|
1281
|
-
info(symbol) {
|
|
1282
|
-
return getSymbolInfo(this._ctx, symbol);
|
|
1283
|
-
}
|
|
1284
|
-
/** Get order size limits and stop/limit distances for all symbols. */
|
|
1285
|
-
limits() {
|
|
1286
|
-
return getSymbolLimits(this._ctx);
|
|
1287
|
-
}
|
|
1288
|
-
};
|
|
1289
|
-
var InstrumentsDomain = class {
|
|
1290
|
-
constructor(_ctx) {
|
|
1291
|
-
this._ctx = _ctx;
|
|
1292
|
-
}
|
|
1293
|
-
/** Get all available instruments, optionally filtered by partial match (e.g. `{ type: "FOREX" }`). */
|
|
1294
|
-
get(params = {}) {
|
|
1295
|
-
return getInstruments(this._ctx, params);
|
|
1296
|
-
}
|
|
1297
|
-
};
|
|
1298
|
-
var OhlcDomain = class {
|
|
1299
|
-
constructor(_ctx) {
|
|
1300
|
-
this._ctx = _ctx;
|
|
1301
|
-
}
|
|
1302
|
-
/**
|
|
1303
|
-
* Fetch OHLC price bars for a symbol.
|
|
1304
|
-
* @param params.symbol - Instrument symbol (e.g. "EURUSD")
|
|
1305
|
-
* @param params.resolution - Bar period in seconds (default: 60 = 1 min)
|
|
1306
|
-
* @param params.range - Lookback window in seconds (default: 432000 = 5 days)
|
|
1307
|
-
* @param params.maxBars - Maximum bars to return (default: 3500)
|
|
1308
|
-
* @param params.priceField - "bid" or "ask" (default: "bid")
|
|
1309
|
-
*/
|
|
1310
|
-
get(params) {
|
|
1311
|
-
return getOHLC(this._ctx, params);
|
|
1476
|
+
/** Hit the broker page to collect Cloudflare cookies before making API calls. */
|
|
1477
|
+
async _preflight() {
|
|
1478
|
+
try {
|
|
1479
|
+
const response = await retryRequest(
|
|
1480
|
+
{
|
|
1481
|
+
method: "GET",
|
|
1482
|
+
url: this._ctx.broker,
|
|
1483
|
+
headers: { ...baseHeaders(), Referer: this._ctx.broker }
|
|
1484
|
+
},
|
|
1485
|
+
this._ctx.retries
|
|
1486
|
+
);
|
|
1487
|
+
const setCookies = response.headers["set-cookie"] ?? [];
|
|
1488
|
+
const incoming = Cookies.parse(setCookies);
|
|
1489
|
+
this._ctx.cookies = Cookies.merge(this._ctx.cookies, incoming);
|
|
1490
|
+
} catch {
|
|
1491
|
+
}
|
|
1312
1492
|
}
|
|
1313
|
-
/**
|
|
1314
|
-
|
|
1315
|
-
|
|
1493
|
+
/** Authenticate and establish a session: login, fetch CSRF, WebSocket handshake, and optional account switch. */
|
|
1494
|
+
async auth() {
|
|
1495
|
+
await this._preflight();
|
|
1496
|
+
await this.login();
|
|
1497
|
+
await this.fetchCsrf();
|
|
1498
|
+
if (this._ctx.debug) clearDebugLog();
|
|
1499
|
+
const cookieStr = Cookies.serialize(this._ctx.cookies);
|
|
1500
|
+
const handshake = await waitForHandshake(endpoints.websocket(this._ctx.broker), cookieStr, 3e4, this._ctx.debug);
|
|
1501
|
+
this._ctx.atmosphereId = handshake.atmosphereId;
|
|
1502
|
+
this._ctx.accountId = handshake.accountId;
|
|
1503
|
+
if (this._ctx.config.accountId) {
|
|
1504
|
+
await this.switchAccount(this._ctx.config.accountId);
|
|
1505
|
+
const reconnect = await waitForHandshake(
|
|
1506
|
+
endpoints.websocket(this._ctx.broker, this._ctx.atmosphereId),
|
|
1507
|
+
Cookies.serialize(this._ctx.cookies),
|
|
1508
|
+
3e4,
|
|
1509
|
+
this._ctx.debug
|
|
1510
|
+
);
|
|
1511
|
+
this._ctx.atmosphereId = reconnect.atmosphereId;
|
|
1512
|
+
this._ctx.accountId = reconnect.accountId;
|
|
1513
|
+
}
|
|
1316
1514
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1515
|
+
/** Connect to the broker with a persistent WebSocket: auth + persistent WS for data reuse and streaming. */
|
|
1516
|
+
async connect() {
|
|
1517
|
+
await this.auth();
|
|
1518
|
+
const wsManager = new WsManager();
|
|
1519
|
+
const wsUrl = endpoints.websocket(this._ctx.broker, this._ctx.atmosphereId);
|
|
1520
|
+
const cookieStr = Cookies.serialize(this._ctx.cookies);
|
|
1521
|
+
await wsManager.connect(wsUrl, cookieStr, this._ctx.debug);
|
|
1522
|
+
this._ctx.wsManager = wsManager;
|
|
1321
1523
|
}
|
|
1322
|
-
/**
|
|
1323
|
-
|
|
1324
|
-
|
|
1524
|
+
/** Close the persistent WebSocket connection. */
|
|
1525
|
+
disconnect() {
|
|
1526
|
+
if (this._ctx.wsManager) {
|
|
1527
|
+
this._ctx.wsManager.close();
|
|
1528
|
+
this._ctx.wsManager = null;
|
|
1529
|
+
}
|
|
1325
1530
|
}
|
|
1326
1531
|
};
|
|
1532
|
+
|
|
1533
|
+
// src/client.ts
|
|
1327
1534
|
var DxtradeClient = class {
|
|
1328
1535
|
_ctx;
|
|
1536
|
+
_session;
|
|
1329
1537
|
/** Position operations: get, close, metrics, streaming. */
|
|
1330
1538
|
positions;
|
|
1331
1539
|
/** Order operations: get, submit, cancel. */
|
|
@@ -1364,6 +1572,7 @@ var DxtradeClient = class {
|
|
|
1364
1572
|
throw error;
|
|
1365
1573
|
}
|
|
1366
1574
|
};
|
|
1575
|
+
this._session = new SessionDomain(this._ctx);
|
|
1367
1576
|
this.positions = new PositionsDomain(this._ctx);
|
|
1368
1577
|
this.orders = new OrdersDomain(this._ctx);
|
|
1369
1578
|
this.account = new AccountDomain(this._ctx);
|
|
@@ -1374,27 +1583,27 @@ var DxtradeClient = class {
|
|
|
1374
1583
|
}
|
|
1375
1584
|
/** Authenticate with the broker using username and password. */
|
|
1376
1585
|
async login() {
|
|
1377
|
-
return login(
|
|
1586
|
+
return this._session.login();
|
|
1378
1587
|
}
|
|
1379
1588
|
/** Fetch the CSRF token required for authenticated requests. */
|
|
1380
1589
|
async fetchCsrf() {
|
|
1381
|
-
return fetchCsrf(
|
|
1590
|
+
return this._session.fetchCsrf();
|
|
1382
1591
|
}
|
|
1383
1592
|
/** Switch to a specific trading account by ID. */
|
|
1384
1593
|
async switchAccount(accountId) {
|
|
1385
|
-
return switchAccount(
|
|
1594
|
+
return this._session.switchAccount(accountId);
|
|
1386
1595
|
}
|
|
1387
1596
|
/** Authenticate and establish a session: login, fetch CSRF, WebSocket handshake, and optional account switch. */
|
|
1388
1597
|
async auth() {
|
|
1389
|
-
return auth(
|
|
1598
|
+
return this._session.auth();
|
|
1390
1599
|
}
|
|
1391
1600
|
/** Connect to the broker with a persistent WebSocket: auth + persistent WS for data reuse and streaming. */
|
|
1392
1601
|
async connect() {
|
|
1393
|
-
return connect(
|
|
1602
|
+
return this._session.connect();
|
|
1394
1603
|
}
|
|
1395
1604
|
/** Close the persistent WebSocket connection. */
|
|
1396
1605
|
disconnect() {
|
|
1397
|
-
return disconnect(
|
|
1606
|
+
return this._session.disconnect();
|
|
1398
1607
|
}
|
|
1399
1608
|
};
|
|
1400
1609
|
export {
|
|
@@ -1403,6 +1612,9 @@ export {
|
|
|
1403
1612
|
DxtradeClient,
|
|
1404
1613
|
DxtradeError,
|
|
1405
1614
|
ERROR,
|
|
1615
|
+
MESSAGE_CATEGORY,
|
|
1616
|
+
MESSAGE_TYPE,
|
|
1617
|
+
ORDER_STATUS,
|
|
1406
1618
|
ORDER_TYPE,
|
|
1407
1619
|
SIDE,
|
|
1408
1620
|
TIF,
|