@danielgroen/dxtrade-api 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -36,6 +36,8 @@ __export(index_exports, {
36
36
  DxtradeError: () => DxtradeError,
37
37
  ORDER_TYPE: () => ORDER_TYPE,
38
38
  SIDE: () => SIDE,
39
+ TIF: () => TIF,
40
+ WS_MESSAGE: () => WS_MESSAGE,
39
41
  endpoints: () => endpoints,
40
42
  resolveBrokerUrl: () => resolveBrokerUrl
41
43
  });
@@ -43,13 +45,11 @@ module.exports = __toCommonJS(index_exports);
43
45
 
44
46
  // src/constants/brokers.ts
45
47
  var BROKER = {
46
- LARK: "https://trade.gooeytrade.com",
48
+ LARKFUNDING: "https://trade.gooeytrade.com",
47
49
  EIGHTCAP: "https://trader.dx-eightcap.com"
48
50
  };
49
51
  function resolveBrokerUrl(broker, customUrls) {
50
- if (customUrls?.[broker]) {
51
- return customUrls[broker];
52
- }
52
+ if (customUrls?.[broker]) return customUrls[broker];
53
53
  const key = broker.toUpperCase();
54
54
  if (BROKER[key]) {
55
55
  return BROKER[key];
@@ -58,6 +58,7 @@ function resolveBrokerUrl(broker, customUrls) {
58
58
  }
59
59
 
60
60
  // src/constants/endpoints.ts
61
+ var websocketQuery = `?X-Atmosphere-tracking-id=0&X-Atmosphere-Framework=2.3.2-javascript&X-Atmosphere-Transport=websocket&X-Atmosphere-TrackMessageSize=true&Content-Type=text/x-gwt-rpc;%20charset=UTF-8&X-atmo-protocol=true&sessionState=dx-new&guest-mode=false`;
61
62
  var endpoints = {
62
63
  login: (base) => `${base}/api/auth/login`,
63
64
  switchAccount: (base, id) => `${base}/api/accounts/switch?accountId=${id}`,
@@ -65,7 +66,7 @@ var endpoints = {
65
66
  instrumentInfo: (base, symbol, tzOffset) => `${base}/api/instruments/info?symbol=${symbol}&timezoneOffset=${tzOffset}&withExDividends=true`,
66
67
  submitOrder: (base) => `${base}/api/orders/single`,
67
68
  assessments: (base) => `${base}/api/assessments`,
68
- websocket: (base) => `wss://${base.split("//")[1]}/client/connector?X-Atmosphere-tracking-id=0&X-Atmosphere-Framework=2.3.2-javascript&X-Atmosphere-Transport=websocket&X-Atmosphere-TrackMessageSize=true&Content-Type=text/x-gwt-rpc;%20charset=UTF-8&X-atmo-protocol=true&sessionState=dx-new&guest-mode=false`
69
+ websocket: (base) => `wss://${base.split("//")[1]}/client/connector` + websocketQuery
69
70
  };
70
71
 
71
72
  // src/constants/enums.ts
@@ -85,6 +86,27 @@ var ACTION = /* @__PURE__ */ ((ACTION2) => {
85
86
  ACTION2["CLOSING"] = "CLOSING";
86
87
  return ACTION2;
87
88
  })(ACTION || {});
89
+ var TIF = /* @__PURE__ */ ((TIF2) => {
90
+ TIF2["GTC"] = "GTC";
91
+ TIF2["DAY"] = "DAY";
92
+ TIF2["GTD"] = "GTD";
93
+ return TIF2;
94
+ })(TIF || {});
95
+ var WS_MESSAGE = /* @__PURE__ */ ((WS_MESSAGE2) => {
96
+ WS_MESSAGE2["ACCOUNT_METRICS"] = "ACCOUNT_METRICS";
97
+ WS_MESSAGE2["ACCOUNTS"] = "ACCOUNTS";
98
+ WS_MESSAGE2["AVAILABLE_WATCHLISTS"] = "AVAILABLE_WATCHLISTS";
99
+ WS_MESSAGE2["INSTRUMENTS"] = "INSTRUMENTS";
100
+ WS_MESSAGE2["LIMITS"] = "LIMITS";
101
+ WS_MESSAGE2["MESSAGE"] = "MESSAGE";
102
+ WS_MESSAGE2["ORDERS"] = "ORDERS";
103
+ WS_MESSAGE2["POSITIONS"] = "POSITIONS";
104
+ WS_MESSAGE2["POSITION_CASH_TRANSFERS"] = "POSITION_CASH_TRANSFERS";
105
+ WS_MESSAGE2["PRIVATE_LAYOUT_NAMES"] = "PRIVATE_LAYOUT_NAMES";
106
+ WS_MESSAGE2["SHARED_PROPERTIES_MESSAGE"] = "SHARED_PROPERTIES_MESSAGE";
107
+ WS_MESSAGE2["USER_LOGIN_INFO"] = "USER_LOGIN_INFO";
108
+ return WS_MESSAGE2;
109
+ })(WS_MESSAGE || {});
88
110
 
89
111
  // src/constants/errors.ts
90
112
  var DxtradeError = class extends Error {
@@ -96,28 +118,30 @@ var DxtradeError = class extends Error {
96
118
  }
97
119
  };
98
120
 
99
- // src/client.ts
100
- var import_crypto = __toESM(require("crypto"));
121
+ // src/domains/account/account.ts
122
+ var import_ws = __toESM(require("ws"));
101
123
 
102
124
  // src/utils/cookies.ts
103
- function parseCookies(setCookieHeaders) {
104
- const cookies = {};
105
- for (const cookie of setCookieHeaders) {
106
- const [nameValue] = cookie.split(";");
107
- const eqIndex = nameValue.indexOf("=");
108
- if (eqIndex === -1) continue;
109
- const name = nameValue.slice(0, eqIndex).trim();
110
- const value = nameValue.slice(eqIndex + 1).trim();
111
- cookies[name] = value;
125
+ var Cookies = class {
126
+ static parse(setCookieHeaders) {
127
+ const cookies = {};
128
+ for (const cookie of setCookieHeaders) {
129
+ const [nameValue] = cookie.split(";");
130
+ const eqIndex = nameValue.indexOf("=");
131
+ if (eqIndex === -1) continue;
132
+ const name = nameValue.slice(0, eqIndex).trim();
133
+ const value = nameValue.slice(eqIndex + 1).trim();
134
+ cookies[name] = value;
135
+ }
136
+ return cookies;
112
137
  }
113
- return cookies;
114
- }
115
- function serializeCookies(cookies) {
116
- return Object.entries(cookies).map(([key, value]) => `${key}=${value}`).join("; ");
117
- }
118
- function mergeCookies(existing, incoming) {
119
- return { ...existing, ...incoming };
120
- }
138
+ static serialize(cookies) {
139
+ return Object.entries(cookies).map(([key, value]) => `${key}=${value}`).join("; ");
140
+ }
141
+ static merge(existing, incoming) {
142
+ return { ...existing, ...incoming };
143
+ }
144
+ };
121
145
 
122
146
  // src/utils/headers.ts
123
147
  function baseHeaders() {
@@ -156,362 +180,547 @@ async function retryRequest(config, retries = 3) {
156
180
  }
157
181
 
158
182
  // src/utils/websocket.ts
159
- var import_ws = __toESM(require("ws"));
160
- function waitForHandshake(wsUrl, cookieStr, timeout = 3e4, debug = false) {
183
+ var import_fs = require("fs");
184
+ var DEBUG_LOG = "debug.log";
185
+ function shouldLog(msg, debug) {
186
+ if (!debug) return false;
187
+ if (debug === true || debug === "true") return true;
188
+ if (typeof msg === "string") return false;
189
+ const filters = debug.split(",").map((s) => s.trim().toUpperCase());
190
+ return filters.includes(msg.type);
191
+ }
192
+ function debugLog(msg) {
193
+ (0, import_fs.appendFileSync)(DEBUG_LOG, JSON.stringify(msg) + "\n");
194
+ }
195
+ function clearDebugLog() {
196
+ (0, import_fs.writeFileSync)(DEBUG_LOG, "");
197
+ }
198
+ function parseWsData(data) {
199
+ const raw = data.toString();
200
+ const pipeIndex = raw.indexOf("|");
201
+ if (pipeIndex === -1) return raw;
202
+ try {
203
+ return JSON.parse(raw.slice(pipeIndex + 1));
204
+ } catch {
205
+ return raw;
206
+ }
207
+ }
208
+
209
+ // src/domains/account/account.ts
210
+ async function getAccountMetrics(ctx, timeout = 3e4) {
211
+ ctx.ensureSession();
212
+ const wsUrl = endpoints.websocket(ctx.baseUrl);
213
+ const cookieStr = Cookies.serialize(ctx.cookies);
161
214
  return new Promise((resolve, reject) => {
162
215
  const ws = new import_ws.default(wsUrl, { headers: { Cookie: cookieStr } });
163
216
  const timer = setTimeout(() => {
164
217
  ws.close();
165
- reject(new Error("[dxtrade-api] Handshake timed out"));
218
+ reject(new DxtradeError("ACCOUNT_METRICS_TIMEOUT", "Account metrics timed out"));
166
219
  }, timeout);
167
220
  ws.on("message", (data) => {
168
- const str = data.toString();
169
- if (debug) console.log("[dxtrade-api:ws]", str);
170
- if (str.includes(`"POSITIONS"`)) {
221
+ const msg = parseWsData(data);
222
+ if (shouldLog(msg, ctx.debug)) debugLog(msg);
223
+ if (typeof msg === "string") return;
224
+ if (msg.type === "ACCOUNT_METRICS" /* ACCOUNT_METRICS */) {
171
225
  clearTimeout(timer);
172
226
  ws.close();
173
- resolve();
227
+ const body = msg.body;
228
+ resolve(body.allMetrics);
174
229
  }
175
230
  });
176
231
  ws.on("error", (error) => {
177
232
  clearTimeout(timer);
178
233
  ws.close();
179
- reject(new Error(`[dxtrade-api] WebSocket handshake error: ${error.message}`));
234
+ reject(new DxtradeError("ACCOUNT_METRICS_ERROR", `Account metrics error: ${error.message}`));
180
235
  });
181
236
  });
182
237
  }
183
- function waitForOrderUpdate(wsUrl, cookieStr, timeout = 3e4, debug = false) {
238
+
239
+ // src/domains/assessments/assessments.ts
240
+ async function getAssessments(ctx, params) {
241
+ ctx.ensureSession();
242
+ try {
243
+ const response = await retryRequest(
244
+ {
245
+ method: "POST",
246
+ url: endpoints.assessments(ctx.baseUrl),
247
+ data: {
248
+ from: params.from,
249
+ instrument: params.instrument,
250
+ subtype: params.subtype ?? null,
251
+ to: params.to
252
+ },
253
+ headers: authHeaders(ctx.csrf, Cookies.serialize(ctx.cookies))
254
+ },
255
+ ctx.retries
256
+ );
257
+ return response.data;
258
+ } catch (error) {
259
+ if (error instanceof DxtradeError) throw error;
260
+ const message = error instanceof Error ? error.message : "Unknown error";
261
+ ctx.throwError("ASSESSMENTS_ERROR", `Error fetching assessments: ${message}`);
262
+ }
263
+ }
264
+
265
+ // src/domains/instrument/instrument.ts
266
+ var import_ws2 = __toESM(require("ws"));
267
+ async function getInstruments(ctx, params = {}, timeout = 3e4) {
268
+ ctx.ensureSession();
269
+ const wsUrl = endpoints.websocket(ctx.baseUrl);
270
+ const cookieStr = Cookies.serialize(ctx.cookies);
184
271
  return new Promise((resolve, reject) => {
185
- const ws = new import_ws.default(wsUrl, { headers: { Cookie: cookieStr } });
272
+ const ws = new import_ws2.default(wsUrl, { headers: { Cookie: cookieStr } });
186
273
  const timer = setTimeout(() => {
274
+ ws.close();
275
+ reject(new DxtradeError("INSTRUMENTS_TIMEOUT", "Instruments request timed out"));
276
+ }, timeout);
277
+ let instruments = [];
278
+ let settleTimer = null;
279
+ ws.on("message", (data) => {
280
+ const msg = parseWsData(data);
281
+ if (shouldLog(msg, ctx.debug)) debugLog(msg);
282
+ if (typeof msg === "string") return;
283
+ if (msg.type === "INSTRUMENTS" /* INSTRUMENTS */) {
284
+ instruments.push(...msg.body);
285
+ if (settleTimer) clearTimeout(settleTimer);
286
+ settleTimer = setTimeout(() => {
287
+ clearTimeout(timer);
288
+ ws.close();
289
+ resolve(
290
+ instruments.filter((instrument) => {
291
+ for (const key in params) {
292
+ if (params[key] !== instrument[key]) {
293
+ return false;
294
+ }
295
+ }
296
+ return true;
297
+ })
298
+ );
299
+ }, 0);
300
+ }
301
+ });
302
+ ws.on("error", (error) => {
303
+ clearTimeout(timer);
304
+ ws.close();
305
+ reject(new DxtradeError("INSTRUMENTS_ERROR", `Instruments error: ${error.message}`));
306
+ });
307
+ });
308
+ }
309
+
310
+ // src/domains/order/order.ts
311
+ var import_crypto = __toESM(require("crypto"));
312
+ var import_ws4 = __toESM(require("ws"));
313
+
314
+ // src/domains/symbol/symbol.ts
315
+ var import_ws3 = __toESM(require("ws"));
316
+ async function getSymbolSuggestions(ctx, text) {
317
+ ctx.ensureSession();
318
+ try {
319
+ const cookieStr = Cookies.serialize(ctx.cookies);
320
+ const response = await retryRequest(
321
+ {
322
+ method: "GET",
323
+ url: endpoints.suggest(ctx.baseUrl, text),
324
+ headers: { ...baseHeaders(), Cookie: cookieStr }
325
+ },
326
+ ctx.retries
327
+ );
328
+ const suggests = response.data?.suggests;
329
+ if (!suggests?.length) {
330
+ ctx.throwError("NO_SUGGESTIONS", "No symbol suggestions found");
331
+ }
332
+ return suggests;
333
+ } catch (error) {
334
+ if (error instanceof DxtradeError) throw error;
335
+ const message = error instanceof Error ? error.message : "Unknown error";
336
+ ctx.throwError("SUGGEST_ERROR", `Error getting symbol suggestions: ${message}`);
337
+ }
338
+ }
339
+ async function getSymbolInfo(ctx, symbol) {
340
+ ctx.ensureSession();
341
+ try {
342
+ const offsetMinutes = Math.abs((/* @__PURE__ */ new Date()).getTimezoneOffset());
343
+ const cookieStr = Cookies.serialize(ctx.cookies);
344
+ const response = await retryRequest(
345
+ {
346
+ method: "GET",
347
+ url: endpoints.instrumentInfo(ctx.baseUrl, symbol, offsetMinutes),
348
+ headers: { ...baseHeaders(), Cookie: cookieStr }
349
+ },
350
+ ctx.retries
351
+ );
352
+ if (!response.data) {
353
+ ctx.throwError("NO_SYMBOL_INFO", "No symbol info returned");
354
+ }
355
+ return response.data;
356
+ } catch (error) {
357
+ if (error instanceof DxtradeError) throw error;
358
+ const message = error instanceof Error ? error.message : "Unknown error";
359
+ ctx.throwError("SYMBOL_INFO_ERROR", `Error getting symbol info: ${message}`);
360
+ }
361
+ }
362
+ async function getSymbolLimits(ctx, timeout = 3e4) {
363
+ ctx.ensureSession();
364
+ const wsUrl = endpoints.websocket(ctx.baseUrl);
365
+ const cookieStr = Cookies.serialize(ctx.cookies);
366
+ return new Promise((resolve, reject) => {
367
+ const ws = new import_ws3.default(wsUrl, { headers: { Cookie: cookieStr } });
368
+ const timer = setTimeout(() => {
369
+ ws.close();
370
+ reject(new DxtradeError("LIMITS_TIMEOUT", "Symbol limits request timed out"));
371
+ }, timeout);
372
+ let limits = [];
373
+ let settleTimer = null;
374
+ ws.on("message", (data) => {
375
+ const msg = parseWsData(data);
376
+ if (shouldLog(msg, ctx.debug)) debugLog(msg);
377
+ if (typeof msg === "string") return;
378
+ if (msg.type === "LIMITS" /* LIMITS */) {
379
+ const batch = msg.body;
380
+ if (batch.length === 0) return;
381
+ limits.push(...batch);
382
+ if (settleTimer) clearTimeout(settleTimer);
383
+ settleTimer = setTimeout(() => {
384
+ clearTimeout(timer);
385
+ ws.close();
386
+ resolve(limits);
387
+ }, 0);
388
+ }
389
+ });
390
+ ws.on("error", (error) => {
391
+ clearTimeout(timer);
392
+ ws.close();
393
+ reject(new DxtradeError("LIMITS_ERROR", `Symbol limits error: ${error.message}`));
394
+ });
395
+ });
396
+ }
397
+
398
+ // src/domains/order/order.ts
399
+ function createOrderListener(wsUrl, cookieStr, timeout = 3e4, debug = false) {
400
+ const ws = new import_ws4.default(wsUrl, { headers: { Cookie: cookieStr } });
401
+ let settled = false;
402
+ const ready = new Promise((resolve) => {
403
+ ws.on("open", resolve);
404
+ });
405
+ const promise = new Promise((resolve, reject) => {
406
+ const timer = setTimeout(() => {
407
+ if (settled) return;
408
+ settled = true;
187
409
  ws.close();
188
410
  reject(new Error("[dxtrade-api] Order update timed out"));
189
411
  }, timeout);
412
+ function done(err, result) {
413
+ if (settled) return;
414
+ settled = true;
415
+ clearTimeout(timer);
416
+ ws.close();
417
+ if (err) reject(err);
418
+ else resolve(result);
419
+ }
190
420
  ws.on("message", (data) => {
191
- const str = data.toString();
192
- if (debug) console.log("[dxtrade-api:ws]", str);
193
- if (!str.includes(`"ORDERS"`)) return;
194
- if (!str.includes(`orderId`)) return;
195
- const json = str.replace(/^.*?\{/, "{");
196
- if (!json.includes("{")) return;
197
- try {
198
- const response = JSON.parse(json);
199
- const body = response.body?.[0];
200
- if (!body) return;
201
- clearTimeout(timer);
202
- ws.close();
421
+ const msg = parseWsData(data);
422
+ if (shouldLog(msg, debug)) debugLog(msg);
423
+ if (typeof msg === "string") return;
424
+ if (msg.type === "MESSAGE" /* MESSAGE */) {
425
+ const messages = msg.body;
426
+ const orderMsg = messages?.findLast?.(
427
+ (m) => m.messageCategory === "TRADE_LOG" && m.messageType === "ORDER" && !m.historyMessage
428
+ );
429
+ if (!orderMsg) return;
430
+ const params = orderMsg.parametersTO;
431
+ if (params.orderStatus === "REJECTED") {
432
+ const reason = params.rejectReason?.key ?? "Unknown reason";
433
+ done(new Error(`[dxtrade-api] Order rejected: ${reason}`));
434
+ } else if (params.orderStatus === "FILLED") {
435
+ done(null, {
436
+ orderId: params.orderKey,
437
+ status: params.orderStatus,
438
+ symbol: params.symbol,
439
+ filledQuantity: params.filledQuantity,
440
+ filledPrice: params.filledPrice
441
+ });
442
+ }
443
+ return;
444
+ }
445
+ if (msg.type === "ORDERS" /* ORDERS */) {
446
+ const body = msg.body?.[0];
447
+ if (!body?.orderId) return;
203
448
  if (body.status === "REJECTED") {
204
- reject(new Error(`[dxtrade-api] Order rejected: ${body.statusDescription ?? "Unknown reason"}`));
205
- } else {
206
- resolve(body);
449
+ done(new Error(`[dxtrade-api] Order rejected: ${body.statusDescription ?? "Unknown reason"}`));
450
+ } else if (body.status === "FILLED") {
451
+ done(null, body);
207
452
  }
208
- } catch {
209
453
  }
210
454
  });
211
455
  ws.on("error", (error) => {
456
+ if (settled) return;
457
+ settled = true;
212
458
  clearTimeout(timer);
213
459
  ws.close();
214
460
  reject(new Error(`[dxtrade-api] WebSocket order listener error: ${error.message}`));
215
461
  });
216
462
  });
463
+ return { promise, ready };
464
+ }
465
+ async function submitOrder(ctx, params) {
466
+ ctx.ensureSession();
467
+ const {
468
+ symbol,
469
+ side,
470
+ quantity,
471
+ orderType,
472
+ orderCode,
473
+ price,
474
+ instrumentId,
475
+ stopLoss,
476
+ takeProfit,
477
+ positionEffect = "OPENING" /* OPENING */,
478
+ positionCode,
479
+ tif = "GTC",
480
+ expireDate,
481
+ metadata
482
+ } = params;
483
+ const info = await getSymbolInfo(ctx, symbol);
484
+ const units = Math.round(quantity * info.lotSize);
485
+ const qty = side === "BUY" /* BUY */ ? units : -units;
486
+ const priceParam = orderType === "STOP" /* STOP */ ? "stopPrice" : "limitPrice";
487
+ const orderData = {
488
+ directExchange: false,
489
+ legs: [
490
+ {
491
+ ...instrumentId != null && { instrumentId },
492
+ ...positionCode != null && { positionCode },
493
+ positionEffect,
494
+ ratioQuantity: 1,
495
+ symbol
496
+ }
497
+ ],
498
+ orderSide: side,
499
+ orderType,
500
+ quantity: qty,
501
+ requestId: orderCode ?? `gwt-uid-931-${import_crypto.default.randomUUID()}`,
502
+ timeInForce: tif,
503
+ ...expireDate != null && { expireDate },
504
+ ...metadata != null && { metadata }
505
+ };
506
+ if (price != null && orderType !== "MARKET" /* MARKET */) {
507
+ orderData[priceParam] = price;
508
+ }
509
+ if (stopLoss) {
510
+ orderData.stopLoss = {
511
+ ...stopLoss.offset != null && { fixedOffset: stopLoss.offset },
512
+ ...stopLoss.price != null && { fixedPrice: stopLoss.price },
513
+ priceFixed: stopLoss.price != null,
514
+ orderChainId: 0,
515
+ orderId: 0,
516
+ orderType: "STOP" /* STOP */,
517
+ quantityForProtection: qty,
518
+ removed: false
519
+ };
520
+ }
521
+ if (takeProfit) {
522
+ orderData.takeProfit = {
523
+ ...takeProfit.offset != null && { fixedOffset: takeProfit.offset },
524
+ ...takeProfit.price != null && { fixedPrice: takeProfit.price },
525
+ priceFixed: takeProfit.price != null,
526
+ orderChainId: 0,
527
+ orderId: 0,
528
+ orderType: "LIMIT" /* LIMIT */,
529
+ quantityForProtection: qty,
530
+ removed: false
531
+ };
532
+ }
533
+ try {
534
+ const wsUrl = endpoints.websocket(ctx.baseUrl);
535
+ const cookieStr = Cookies.serialize(ctx.cookies);
536
+ const listener = createOrderListener(wsUrl, cookieStr, 3e4, ctx.debug);
537
+ await listener.ready;
538
+ const response = await retryRequest(
539
+ {
540
+ method: "POST",
541
+ url: endpoints.submitOrder(ctx.baseUrl),
542
+ data: orderData,
543
+ headers: authHeaders(ctx.csrf, Cookies.serialize(ctx.cookies))
544
+ },
545
+ ctx.retries
546
+ );
547
+ ctx.callbacks.onOrderPlaced?.(response.data);
548
+ const orderUpdate = await listener.promise;
549
+ ctx.callbacks.onOrderUpdate?.(orderUpdate);
550
+ return orderUpdate;
551
+ } catch (error) {
552
+ if (error instanceof DxtradeError) throw error;
553
+ const message = error instanceof Error ? error.response?.data?.message ?? error.message : "Unknown error";
554
+ ctx.throwError("ORDER_ERROR", `Error submitting order: ${message}`);
555
+ }
556
+ }
557
+
558
+ // src/domains/session/session.ts
559
+ var import_ws5 = __toESM(require("ws"));
560
+ function waitForHandshake(wsUrl, cookieStr, timeout = 3e4, debug = false) {
561
+ return new Promise((resolve, reject) => {
562
+ const ws = new import_ws5.default(wsUrl, { headers: { Cookie: cookieStr } });
563
+ const timer = setTimeout(() => {
564
+ ws.close();
565
+ reject(new Error("[dxtrade-api] Handshake timed out"));
566
+ }, timeout);
567
+ ws.on("message", (data) => {
568
+ const msg = parseWsData(data);
569
+ if (shouldLog(msg, debug)) debugLog(msg);
570
+ if (typeof msg === "string") return;
571
+ if (msg.type === "POSITIONS" /* POSITIONS */) {
572
+ clearTimeout(timer);
573
+ ws.close();
574
+ resolve();
575
+ }
576
+ });
577
+ ws.on("error", (error) => {
578
+ clearTimeout(timer);
579
+ ws.close();
580
+ reject(new Error(`[dxtrade-api] WebSocket handshake error: ${error.message}`));
581
+ });
582
+ });
583
+ }
584
+ async function login(ctx) {
585
+ try {
586
+ const response = await retryRequest(
587
+ {
588
+ method: "POST",
589
+ url: endpoints.login(ctx.baseUrl),
590
+ data: {
591
+ username: ctx.config.username,
592
+ password: ctx.config.password,
593
+ domain: ctx.config.broker
594
+ },
595
+ headers: { "Content-Type": "application/json" }
596
+ },
597
+ ctx.retries
598
+ );
599
+ if (response.status === 200) {
600
+ const setCookies = response.headers["set-cookie"] ?? [];
601
+ const incoming = Cookies.parse(setCookies);
602
+ ctx.cookies = Cookies.merge(ctx.cookies, incoming);
603
+ ctx.callbacks.onLogin?.();
604
+ } else {
605
+ ctx.throwError("LOGIN_FAILED", `Login failed: ${response.status}`);
606
+ }
607
+ } catch (error) {
608
+ if (error instanceof DxtradeError) throw error;
609
+ const message = error instanceof Error ? error.message : "Unknown error";
610
+ ctx.throwError("LOGIN_ERROR", `Login error: ${message}`);
611
+ }
612
+ }
613
+ async function fetchCsrf(ctx) {
614
+ try {
615
+ const cookieStr = Cookies.serialize(ctx.cookies);
616
+ const response = await retryRequest(
617
+ {
618
+ method: "GET",
619
+ url: ctx.baseUrl,
620
+ headers: { ...cookieOnlyHeaders(cookieStr), Referer: ctx.baseUrl }
621
+ },
622
+ ctx.retries
623
+ );
624
+ const csrfMatch = response.data?.match(/name="csrf" content="([^"]+)"/);
625
+ if (csrfMatch) {
626
+ ctx.csrf = csrfMatch[1];
627
+ } else {
628
+ ctx.throwError("CSRF_NOT_FOUND", "CSRF token not found");
629
+ }
630
+ } catch (error) {
631
+ if (error instanceof DxtradeError) throw error;
632
+ const message = error instanceof Error ? error.message : "Unknown error";
633
+ ctx.throwError("CSRF_ERROR", `CSRF fetch error: ${message}`);
634
+ }
635
+ }
636
+ async function switchAccount(ctx, accountId) {
637
+ ctx.ensureSession();
638
+ try {
639
+ await retryRequest(
640
+ {
641
+ method: "POST",
642
+ url: endpoints.switchAccount(ctx.baseUrl, accountId),
643
+ headers: authHeaders(ctx.csrf, Cookies.serialize(ctx.cookies))
644
+ },
645
+ ctx.retries
646
+ );
647
+ ctx.callbacks.onAccountSwitch?.(accountId);
648
+ } catch (error) {
649
+ if (error instanceof DxtradeError) throw error;
650
+ const message = error instanceof Error ? error.message : "Unknown error";
651
+ ctx.throwError("ACCOUNT_SWITCH_ERROR", `Error switching account: ${message}`);
652
+ }
653
+ }
654
+ async function connect(ctx) {
655
+ await login(ctx);
656
+ await fetchCsrf(ctx);
657
+ if (ctx.debug) clearDebugLog();
658
+ const wsUrl = endpoints.websocket(ctx.baseUrl);
659
+ const cookieStr = Cookies.serialize(ctx.cookies);
660
+ await waitForHandshake(wsUrl, cookieStr, 3e4, ctx.debug);
661
+ if (ctx.config.accountId) {
662
+ await switchAccount(ctx, ctx.config.accountId);
663
+ await waitForHandshake(endpoints.websocket(ctx.baseUrl), Cookies.serialize(ctx.cookies), 3e4, ctx.debug);
664
+ }
217
665
  }
218
666
 
219
667
  // src/client.ts
220
668
  var DxtradeClient = class {
221
- config;
222
- callbacks;
223
- cookies = {};
224
- csrf = null;
225
- baseUrl;
226
- retries;
227
- debug;
669
+ _ctx;
228
670
  constructor(config) {
229
- this.config = config;
230
- this.callbacks = config.callbacks ?? {};
231
- this.baseUrl = resolveBrokerUrl(config.broker, config.brokerUrls);
232
- this.retries = config.retries ?? 3;
233
- this.debug = config.debug ?? false;
671
+ const callbacks = config.callbacks ?? {};
672
+ this._ctx = {
673
+ config,
674
+ callbacks,
675
+ cookies: {},
676
+ csrf: null,
677
+ baseUrl: resolveBrokerUrl(config.broker, config.brokerUrls),
678
+ retries: config.retries ?? 3,
679
+ debug: config.debug ?? false,
680
+ ensureSession() {
681
+ if (!this.csrf) {
682
+ throw new DxtradeError("NO_SESSION", "No active session. Call login() and fetchCsrf() or connect() first.");
683
+ }
684
+ },
685
+ throwError(code, message) {
686
+ const error = new DxtradeError(code, message);
687
+ callbacks.onError?.(error);
688
+ throw error;
689
+ }
690
+ };
234
691
  }
235
- // ── Session ─────────────────────────────────────────────────────────────────────────────────────────────────────────
236
692
  async login() {
237
- try {
238
- const response = await retryRequest(
239
- {
240
- method: "POST",
241
- url: endpoints.login(this.baseUrl),
242
- data: {
243
- username: this.config.username,
244
- password: this.config.password,
245
- domain: this.config.broker
246
- },
247
- headers: { "Content-Type": "application/json" }
248
- },
249
- this.retries
250
- );
251
- if (response.status === 200) {
252
- const setCookies = response.headers["set-cookie"] ?? [];
253
- const incoming = parseCookies(setCookies);
254
- this.cookies = mergeCookies(this.cookies, incoming);
255
- this.callbacks.onLogin?.();
256
- } else {
257
- this.throwError("LOGIN_FAILED", `Login failed: ${response.status}`);
258
- }
259
- } catch (error) {
260
- if (error instanceof DxtradeError) throw error;
261
- const message = error instanceof Error ? error.message : "Unknown error";
262
- this.throwError("LOGIN_ERROR", `Login error: ${message}`);
263
- }
693
+ return login(this._ctx);
264
694
  }
265
695
  async fetchCsrf() {
266
- try {
267
- const cookieStr = serializeCookies(this.cookies);
268
- const response = await retryRequest(
269
- {
270
- method: "GET",
271
- url: this.baseUrl,
272
- headers: { ...cookieOnlyHeaders(cookieStr), Referer: this.baseUrl }
273
- },
274
- this.retries
275
- );
276
- const csrfMatch = response.data?.match(/name="csrf" content="([^"]+)"/);
277
- if (csrfMatch) {
278
- this.csrf = csrfMatch[1];
279
- } else {
280
- this.throwError("CSRF_NOT_FOUND", "CSRF token not found");
281
- }
282
- } catch (error) {
283
- if (error instanceof DxtradeError) throw error;
284
- const message = error instanceof Error ? error.message : "Unknown error";
285
- this.throwError("CSRF_ERROR", `CSRF fetch error: ${message}`);
286
- }
696
+ return fetchCsrf(this._ctx);
287
697
  }
288
698
  async switchAccount(accountId) {
289
- this.ensureSession();
290
- try {
291
- await retryRequest(
292
- {
293
- method: "POST",
294
- url: endpoints.switchAccount(this.baseUrl, accountId),
295
- headers: authHeaders(this.csrf, serializeCookies(this.cookies))
296
- },
297
- this.retries
298
- );
299
- this.callbacks.onAccountSwitch?.(accountId);
300
- } catch (error) {
301
- if (error instanceof DxtradeError) throw error;
302
- const message = error instanceof Error ? error.message : "Unknown error";
303
- this.throwError(
304
- "ACCOUNT_SWITCH_ERROR",
305
- `Error switching account: ${message}`
306
- );
307
- }
699
+ return switchAccount(this._ctx, accountId);
700
+ }
701
+ async connect() {
702
+ return connect(this._ctx);
308
703
  }
309
- // ── Market Data ─────────────────────────────────────────────────────────────────────────────────────────────────────
310
704
  async getSymbolSuggestions(text) {
311
- this.ensureSession();
312
- try {
313
- const cookieStr = serializeCookies(this.cookies);
314
- const response = await retryRequest(
315
- {
316
- method: "GET",
317
- url: endpoints.suggest(this.baseUrl, text),
318
- headers: { ...baseHeaders(), Cookie: cookieStr }
319
- },
320
- this.retries
321
- );
322
- const suggests = response.data?.suggests;
323
- if (!suggests?.length) {
324
- this.throwError("NO_SUGGESTIONS", "No symbol suggestions found");
325
- }
326
- return suggests;
327
- } catch (error) {
328
- if (error instanceof DxtradeError) throw error;
329
- const message = error instanceof Error ? error.message : "Unknown error";
330
- this.throwError(
331
- "SUGGEST_ERROR",
332
- `Error getting symbol suggestions: ${message}`
333
- );
334
- }
705
+ return getSymbolSuggestions(this._ctx, text);
335
706
  }
336
707
  async getSymbolInfo(symbol) {
337
- this.ensureSession();
338
- try {
339
- const offsetMinutes = Math.abs((/* @__PURE__ */ new Date()).getTimezoneOffset());
340
- const cookieStr = serializeCookies(this.cookies);
341
- const response = await retryRequest(
342
- {
343
- method: "GET",
344
- url: endpoints.instrumentInfo(this.baseUrl, symbol, offsetMinutes),
345
- headers: { ...baseHeaders(), Cookie: cookieStr }
346
- },
347
- this.retries
348
- );
349
- if (!response.data) {
350
- this.throwError("NO_SYMBOL_INFO", "No symbol info returned");
351
- }
352
- return response.data;
353
- } catch (error) {
354
- if (error instanceof DxtradeError) throw error;
355
- const message = error instanceof Error ? error.message : "Unknown error";
356
- this.throwError(
357
- "SYMBOL_INFO_ERROR",
358
- `Error getting symbol info: ${message}`
359
- );
360
- }
708
+ return getSymbolInfo(this._ctx, symbol);
361
709
  }
362
- // ── Trading ─────────────────────────────────────────────────────────────────────────────────────────────────────────
363
- async submitOrder(params) {
364
- this.ensureSession();
365
- const {
366
- symbol,
367
- side,
368
- quantity,
369
- orderType,
370
- price,
371
- instrumentId,
372
- stopLoss,
373
- takeProfit,
374
- positionEffect = "OPENING" /* OPENING */
375
- } = params;
376
- const qty = side === "BUY" /* BUY */ ? Math.abs(quantity) : -Math.abs(quantity);
377
- const priceParam = orderType === "STOP" /* STOP */ ? "stopPrice" : "limitPrice";
378
- const orderData = {
379
- directExchange: false,
380
- legs: [
381
- {
382
- ...instrumentId != null && { instrumentId },
383
- positionEffect,
384
- ratioQuantity: 1,
385
- symbol
386
- }
387
- ],
388
- orderSide: side,
389
- orderType,
390
- quantity: qty,
391
- requestId: `gwt-uid-931-${import_crypto.default.randomUUID()}`,
392
- timeInForce: "GTC"
393
- };
394
- if (price != null && orderType !== "MARKET" /* MARKET */) {
395
- orderData[priceParam] = price;
396
- }
397
- if (stopLoss) {
398
- orderData.stopLoss = {
399
- ...stopLoss.offset != null && { fixedOffset: stopLoss.offset },
400
- ...stopLoss.price != null && { fixedPrice: stopLoss.price },
401
- priceFixed: stopLoss.price != null,
402
- orderChainId: 0,
403
- orderId: 0,
404
- orderType: "STOP" /* STOP */,
405
- quantityForProtection: qty,
406
- removed: false
407
- };
408
- }
409
- if (takeProfit) {
410
- orderData.takeProfit = {
411
- ...takeProfit.offset != null && { fixedOffset: takeProfit.offset },
412
- ...takeProfit.price != null && { fixedPrice: takeProfit.price },
413
- priceFixed: takeProfit.price != null,
414
- orderChainId: 0,
415
- orderId: 0,
416
- orderType: "LIMIT" /* LIMIT */,
417
- quantityForProtection: qty,
418
- removed: false
419
- };
420
- }
421
- try {
422
- const response = await retryRequest(
423
- {
424
- method: "POST",
425
- url: endpoints.submitOrder(this.baseUrl),
426
- data: orderData,
427
- headers: authHeaders(this.csrf, serializeCookies(this.cookies))
428
- },
429
- this.retries
430
- );
431
- if (response.status !== 200) {
432
- this.throwError(
433
- "ORDER_SUBMIT_FAILED",
434
- `Order submission failed with status: ${response.status}`
435
- );
436
- }
437
- this.callbacks.onOrderPlaced?.({
438
- status: response.status,
439
- data: response.data
440
- });
441
- const wsUrl = endpoints.websocket(this.baseUrl);
442
- const cookieStr = serializeCookies(this.cookies);
443
- const orderUpdate = await waitForOrderUpdate(
444
- wsUrl,
445
- cookieStr,
446
- 3e4,
447
- this.debug
448
- );
449
- this.callbacks.onOrderUpdate?.(orderUpdate);
450
- return orderUpdate;
451
- } catch (error) {
452
- if (error instanceof DxtradeError) throw error;
453
- const message = error instanceof Error ? error.response?.data?.message ?? error.message : "Unknown error";
454
- this.throwError("ORDER_ERROR", `Error submitting order: ${message}`);
455
- }
710
+ async getSymbolLimits() {
711
+ return getSymbolLimits(this._ctx);
456
712
  }
457
- // ── Analytics ───────────────────────────────────────────────────────────────────────────────────────────────────────
458
- async getAssessments(params) {
459
- this.ensureSession();
460
- try {
461
- const response = await retryRequest(
462
- {
463
- method: "POST",
464
- url: endpoints.assessments(this.baseUrl),
465
- data: {
466
- from: params.from,
467
- instrument: params.instrument,
468
- subtype: params.subtype ?? null,
469
- to: params.to
470
- },
471
- headers: authHeaders(this.csrf, serializeCookies(this.cookies))
472
- },
473
- this.retries
474
- );
475
- return response.data;
476
- } catch (error) {
477
- if (error instanceof DxtradeError) throw error;
478
- const message = error instanceof Error ? error.message : "Unknown error";
479
- this.throwError(
480
- "ASSESSMENTS_ERROR",
481
- `Error fetching assessments: ${message}`
482
- );
483
- }
713
+ async submitOrder(params) {
714
+ return submitOrder(this._ctx, params);
484
715
  }
485
- // ── Convenience ─────────────────────────────────────────────────────────────────────────────────────────────────────
486
- async connect() {
487
- await this.login();
488
- await this.fetchCsrf();
489
- const wsUrl = endpoints.websocket(this.baseUrl);
490
- const cookieStr = serializeCookies(this.cookies);
491
- await waitForHandshake(wsUrl, cookieStr, 3e4, this.debug);
492
- if (this.config.accountId) {
493
- await this.switchAccount(this.config.accountId);
494
- await waitForHandshake(
495
- endpoints.websocket(this.baseUrl),
496
- serializeCookies(this.cookies),
497
- 3e4,
498
- this.debug
499
- );
500
- }
716
+ async getAccountMetrics() {
717
+ return getAccountMetrics(this._ctx);
501
718
  }
502
- // ── Internals ───────────────────────────────────────────────────────────────────────────────────────────────────────
503
- ensureSession() {
504
- if (!this.csrf) {
505
- throw new DxtradeError(
506
- "NO_SESSION",
507
- "No active session. Call login() and fetchCsrf() or connect() first."
508
- );
509
- }
719
+ async getInstruments(params = {}) {
720
+ return getInstruments(this._ctx, params);
510
721
  }
511
- throwError(code, message) {
512
- const error = new DxtradeError(code, message);
513
- this.callbacks.onError?.(error);
514
- throw error;
722
+ async getAssessments(params) {
723
+ return getAssessments(this._ctx, params);
515
724
  }
516
725
  };
517
726
  // Annotate the CommonJS export names for ESM import in node:
@@ -522,6 +731,8 @@ var DxtradeClient = class {
522
731
  DxtradeError,
523
732
  ORDER_TYPE,
524
733
  SIDE,
734
+ TIF,
735
+ WS_MESSAGE,
525
736
  endpoints,
526
737
  resolveBrokerUrl
527
738
  });