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