@danielgroen/dxtrade-api 1.0.7 → 1.0.8
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 +8 -8
- package/dist/index.d.mts +143 -66
- package/dist/index.d.ts +143 -66
- package/dist/index.js +411 -326
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +409 -326
- package/dist/index.mjs.map +1 -1
- package/package.json +21 -7
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
// src/constants/brokers.ts
|
|
2
2
|
var BROKER = {
|
|
3
|
-
|
|
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
|
|
24
|
+
websocket: (base) => `wss://${base.split("//")[1]}/client/connector` + websocketQuery
|
|
26
25
|
};
|
|
27
26
|
|
|
28
27
|
// src/constants/enums.ts
|
|
@@ -42,6 +41,24 @@ 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["ACCOUNTS"] = "ACCOUNTS";
|
|
52
|
+
WS_MESSAGE2["AVAILABLE_WATCHLISTS"] = "AVAILABLE_WATCHLISTS";
|
|
53
|
+
WS_MESSAGE2["MESSAGE"] = "MESSAGE";
|
|
54
|
+
WS_MESSAGE2["ORDERS"] = "ORDERS";
|
|
55
|
+
WS_MESSAGE2["POSITIONS"] = "POSITIONS";
|
|
56
|
+
WS_MESSAGE2["POSITION_CASH_TRANSFERS"] = "POSITION_CASH_TRANSFERS";
|
|
57
|
+
WS_MESSAGE2["PRIVATE_LAYOUT_NAMES"] = "PRIVATE_LAYOUT_NAMES";
|
|
58
|
+
WS_MESSAGE2["SHARED_PROPERTIES_MESSAGE"] = "SHARED_PROPERTIES_MESSAGE";
|
|
59
|
+
WS_MESSAGE2["USER_LOGIN_INFO"] = "USER_LOGIN_INFO";
|
|
60
|
+
return WS_MESSAGE2;
|
|
61
|
+
})(WS_MESSAGE || {});
|
|
45
62
|
|
|
46
63
|
// src/constants/errors.ts
|
|
47
64
|
var DxtradeError = class extends Error {
|
|
@@ -53,28 +70,30 @@ var DxtradeError = class extends Error {
|
|
|
53
70
|
}
|
|
54
71
|
};
|
|
55
72
|
|
|
56
|
-
// src/
|
|
57
|
-
import
|
|
73
|
+
// src/domains/session/session.ts
|
|
74
|
+
import WebSocket from "ws";
|
|
58
75
|
|
|
59
76
|
// src/utils/cookies.ts
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
var Cookies = class {
|
|
78
|
+
static parse(setCookieHeaders) {
|
|
79
|
+
const cookies = {};
|
|
80
|
+
for (const cookie of setCookieHeaders) {
|
|
81
|
+
const [nameValue] = cookie.split(";");
|
|
82
|
+
const eqIndex = nameValue.indexOf("=");
|
|
83
|
+
if (eqIndex === -1) continue;
|
|
84
|
+
const name = nameValue.slice(0, eqIndex).trim();
|
|
85
|
+
const value = nameValue.slice(eqIndex + 1).trim();
|
|
86
|
+
cookies[name] = value;
|
|
87
|
+
}
|
|
88
|
+
return cookies;
|
|
69
89
|
}
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
90
|
+
static serialize(cookies) {
|
|
91
|
+
return Object.entries(cookies).map(([key, value]) => `${key}=${value}`).join("; ");
|
|
92
|
+
}
|
|
93
|
+
static merge(existing, incoming) {
|
|
94
|
+
return { ...existing, ...incoming };
|
|
95
|
+
}
|
|
96
|
+
};
|
|
78
97
|
|
|
79
98
|
// src/utils/headers.ts
|
|
80
99
|
function baseHeaders() {
|
|
@@ -113,7 +132,33 @@ async function retryRequest(config, retries = 3) {
|
|
|
113
132
|
}
|
|
114
133
|
|
|
115
134
|
// src/utils/websocket.ts
|
|
116
|
-
import
|
|
135
|
+
import { appendFileSync, writeFileSync } from "fs";
|
|
136
|
+
var DEBUG_LOG = "debug.log";
|
|
137
|
+
function shouldLog(msg, debug) {
|
|
138
|
+
if (!debug) return false;
|
|
139
|
+
if (debug === true || debug === "true") return true;
|
|
140
|
+
if (typeof msg === "string") return false;
|
|
141
|
+
const filters = debug.split(",").map((s) => s.trim().toUpperCase());
|
|
142
|
+
return filters.includes(msg.type);
|
|
143
|
+
}
|
|
144
|
+
function debugLog(msg) {
|
|
145
|
+
appendFileSync(DEBUG_LOG, JSON.stringify(msg) + "\n");
|
|
146
|
+
}
|
|
147
|
+
function clearDebugLog() {
|
|
148
|
+
writeFileSync(DEBUG_LOG, "");
|
|
149
|
+
}
|
|
150
|
+
function parseWsData(data) {
|
|
151
|
+
const raw = data.toString();
|
|
152
|
+
const pipeIndex = raw.indexOf("|");
|
|
153
|
+
if (pipeIndex === -1) return raw;
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(raw.slice(pipeIndex + 1));
|
|
156
|
+
} catch {
|
|
157
|
+
return raw;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/domains/session/session.ts
|
|
117
162
|
function waitForHandshake(wsUrl, cookieStr, timeout = 3e4, debug = false) {
|
|
118
163
|
return new Promise((resolve, reject) => {
|
|
119
164
|
const ws = new WebSocket(wsUrl, { headers: { Cookie: cookieStr } });
|
|
@@ -122,9 +167,10 @@ function waitForHandshake(wsUrl, cookieStr, timeout = 3e4, debug = false) {
|
|
|
122
167
|
reject(new Error("[dxtrade-api] Handshake timed out"));
|
|
123
168
|
}, timeout);
|
|
124
169
|
ws.on("message", (data) => {
|
|
125
|
-
const
|
|
126
|
-
if (debug)
|
|
127
|
-
if (
|
|
170
|
+
const msg = parseWsData(data);
|
|
171
|
+
if (shouldLog(msg, debug)) debugLog(msg);
|
|
172
|
+
if (typeof msg === "string") return;
|
|
173
|
+
if (msg.type === "POSITIONS" /* POSITIONS */) {
|
|
128
174
|
clearTimeout(timer);
|
|
129
175
|
ws.close();
|
|
130
176
|
resolve();
|
|
@@ -137,338 +183,373 @@ function waitForHandshake(wsUrl, cookieStr, timeout = 3e4, debug = false) {
|
|
|
137
183
|
});
|
|
138
184
|
});
|
|
139
185
|
}
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
const
|
|
186
|
+
async function login(ctx) {
|
|
187
|
+
try {
|
|
188
|
+
const response = await retryRequest(
|
|
189
|
+
{
|
|
190
|
+
method: "POST",
|
|
191
|
+
url: endpoints.login(ctx.baseUrl),
|
|
192
|
+
data: {
|
|
193
|
+
username: ctx.config.username,
|
|
194
|
+
password: ctx.config.password,
|
|
195
|
+
domain: ctx.config.broker
|
|
196
|
+
},
|
|
197
|
+
headers: { "Content-Type": "application/json" }
|
|
198
|
+
},
|
|
199
|
+
ctx.retries
|
|
200
|
+
);
|
|
201
|
+
if (response.status === 200) {
|
|
202
|
+
const setCookies = response.headers["set-cookie"] ?? [];
|
|
203
|
+
const incoming = Cookies.parse(setCookies);
|
|
204
|
+
ctx.cookies = Cookies.merge(ctx.cookies, incoming);
|
|
205
|
+
ctx.callbacks.onLogin?.();
|
|
206
|
+
} else {
|
|
207
|
+
ctx.throwError("LOGIN_FAILED", `Login failed: ${response.status}`);
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (error instanceof DxtradeError) throw error;
|
|
211
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
212
|
+
ctx.throwError("LOGIN_ERROR", `Login error: ${message}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function fetchCsrf(ctx) {
|
|
216
|
+
try {
|
|
217
|
+
const cookieStr = Cookies.serialize(ctx.cookies);
|
|
218
|
+
const response = await retryRequest(
|
|
219
|
+
{
|
|
220
|
+
method: "GET",
|
|
221
|
+
url: ctx.baseUrl,
|
|
222
|
+
headers: { ...cookieOnlyHeaders(cookieStr), Referer: ctx.baseUrl }
|
|
223
|
+
},
|
|
224
|
+
ctx.retries
|
|
225
|
+
);
|
|
226
|
+
const csrfMatch = response.data?.match(/name="csrf" content="([^"]+)"/);
|
|
227
|
+
if (csrfMatch) {
|
|
228
|
+
ctx.csrf = csrfMatch[1];
|
|
229
|
+
} else {
|
|
230
|
+
ctx.throwError("CSRF_NOT_FOUND", "CSRF token not found");
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error instanceof DxtradeError) throw error;
|
|
234
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
235
|
+
ctx.throwError("CSRF_ERROR", `CSRF fetch error: ${message}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function switchAccount(ctx, accountId) {
|
|
239
|
+
ctx.ensureSession();
|
|
240
|
+
try {
|
|
241
|
+
await retryRequest(
|
|
242
|
+
{
|
|
243
|
+
method: "POST",
|
|
244
|
+
url: endpoints.switchAccount(ctx.baseUrl, accountId),
|
|
245
|
+
headers: authHeaders(ctx.csrf, Cookies.serialize(ctx.cookies))
|
|
246
|
+
},
|
|
247
|
+
ctx.retries
|
|
248
|
+
);
|
|
249
|
+
ctx.callbacks.onAccountSwitch?.(accountId);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (error instanceof DxtradeError) throw error;
|
|
252
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
253
|
+
ctx.throwError("ACCOUNT_SWITCH_ERROR", `Error switching account: ${message}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async function connect(ctx) {
|
|
257
|
+
await login(ctx);
|
|
258
|
+
await fetchCsrf(ctx);
|
|
259
|
+
if (ctx.debug) clearDebugLog();
|
|
260
|
+
const wsUrl = endpoints.websocket(ctx.baseUrl);
|
|
261
|
+
const cookieStr = Cookies.serialize(ctx.cookies);
|
|
262
|
+
await waitForHandshake(wsUrl, cookieStr, 3e4, ctx.debug);
|
|
263
|
+
if (ctx.config.accountId) {
|
|
264
|
+
await switchAccount(ctx, ctx.config.accountId);
|
|
265
|
+
await waitForHandshake(endpoints.websocket(ctx.baseUrl), Cookies.serialize(ctx.cookies), 3e4, ctx.debug);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/domains/symbol/symbol.ts
|
|
270
|
+
async function getSymbolSuggestions(ctx, text) {
|
|
271
|
+
ctx.ensureSession();
|
|
272
|
+
try {
|
|
273
|
+
const cookieStr = Cookies.serialize(ctx.cookies);
|
|
274
|
+
const response = await retryRequest(
|
|
275
|
+
{
|
|
276
|
+
method: "GET",
|
|
277
|
+
url: endpoints.suggest(ctx.baseUrl, text),
|
|
278
|
+
headers: { ...baseHeaders(), Cookie: cookieStr }
|
|
279
|
+
},
|
|
280
|
+
ctx.retries
|
|
281
|
+
);
|
|
282
|
+
const suggests = response.data?.suggests;
|
|
283
|
+
if (!suggests?.length) {
|
|
284
|
+
ctx.throwError("NO_SUGGESTIONS", "No symbol suggestions found");
|
|
285
|
+
}
|
|
286
|
+
return suggests;
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if (error instanceof DxtradeError) throw error;
|
|
289
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
290
|
+
ctx.throwError("SUGGEST_ERROR", `Error getting symbol suggestions: ${message}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async function getSymbolInfo(ctx, symbol) {
|
|
294
|
+
ctx.ensureSession();
|
|
295
|
+
try {
|
|
296
|
+
const offsetMinutes = Math.abs((/* @__PURE__ */ new Date()).getTimezoneOffset());
|
|
297
|
+
const cookieStr = Cookies.serialize(ctx.cookies);
|
|
298
|
+
const response = await retryRequest(
|
|
299
|
+
{
|
|
300
|
+
method: "GET",
|
|
301
|
+
url: endpoints.instrumentInfo(ctx.baseUrl, symbol, offsetMinutes),
|
|
302
|
+
headers: { ...baseHeaders(), Cookie: cookieStr }
|
|
303
|
+
},
|
|
304
|
+
ctx.retries
|
|
305
|
+
);
|
|
306
|
+
if (!response.data) {
|
|
307
|
+
ctx.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
|
+
ctx.throwError("SYMBOL_INFO_ERROR", `Error getting symbol info: ${message}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/domains/order/order.ts
|
|
318
|
+
import crypto from "crypto";
|
|
319
|
+
import WebSocket2 from "ws";
|
|
320
|
+
function createOrderListener(wsUrl, cookieStr, timeout = 3e4, debug = false) {
|
|
321
|
+
const ws = new WebSocket2(wsUrl, { headers: { Cookie: cookieStr } });
|
|
322
|
+
let settled = false;
|
|
323
|
+
const ready = new Promise((resolve) => {
|
|
324
|
+
ws.on("open", resolve);
|
|
325
|
+
});
|
|
326
|
+
const promise = new Promise((resolve, reject) => {
|
|
143
327
|
const timer = setTimeout(() => {
|
|
328
|
+
if (settled) return;
|
|
329
|
+
settled = true;
|
|
144
330
|
ws.close();
|
|
145
331
|
reject(new Error("[dxtrade-api] Order update timed out"));
|
|
146
332
|
}, timeout);
|
|
333
|
+
function done(err, result) {
|
|
334
|
+
if (settled) return;
|
|
335
|
+
settled = true;
|
|
336
|
+
clearTimeout(timer);
|
|
337
|
+
ws.close();
|
|
338
|
+
if (err) reject(err);
|
|
339
|
+
else resolve(result);
|
|
340
|
+
}
|
|
147
341
|
ws.on("message", (data) => {
|
|
148
|
-
const
|
|
149
|
-
if (debug)
|
|
150
|
-
if (
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
342
|
+
const msg = parseWsData(data);
|
|
343
|
+
if (shouldLog(msg, debug)) debugLog(msg);
|
|
344
|
+
if (typeof msg === "string") return;
|
|
345
|
+
if (msg.type === "MESSAGE" /* MESSAGE */) {
|
|
346
|
+
const messages = msg.body;
|
|
347
|
+
const orderMsg = messages?.findLast?.(
|
|
348
|
+
(m) => m.messageCategory === "TRADE_LOG" && m.messageType === "ORDER" && !m.historyMessage
|
|
349
|
+
);
|
|
350
|
+
if (!orderMsg) return;
|
|
351
|
+
const params = orderMsg.parametersTO;
|
|
352
|
+
if (params.orderStatus === "REJECTED") {
|
|
353
|
+
const reason = params.rejectReason?.key ?? "Unknown reason";
|
|
354
|
+
done(new Error(`[dxtrade-api] Order rejected: ${reason}`));
|
|
355
|
+
} else if (params.orderStatus === "FILLED") {
|
|
356
|
+
done(null, {
|
|
357
|
+
orderId: params.orderKey,
|
|
358
|
+
status: params.orderStatus,
|
|
359
|
+
symbol: params.symbol,
|
|
360
|
+
filledQuantity: params.filledQuantity,
|
|
361
|
+
filledPrice: params.filledPrice
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (msg.type === "ORDERS" /* ORDERS */) {
|
|
367
|
+
const body = msg.body?.[0];
|
|
368
|
+
if (!body?.orderId) return;
|
|
160
369
|
if (body.status === "REJECTED") {
|
|
161
|
-
|
|
162
|
-
} else {
|
|
163
|
-
|
|
370
|
+
done(new Error(`[dxtrade-api] Order rejected: ${body.statusDescription ?? "Unknown reason"}`));
|
|
371
|
+
} else if (body.status === "FILLED") {
|
|
372
|
+
done(null, body);
|
|
164
373
|
}
|
|
165
|
-
} catch {
|
|
166
374
|
}
|
|
167
375
|
});
|
|
168
376
|
ws.on("error", (error) => {
|
|
377
|
+
if (settled) return;
|
|
378
|
+
settled = true;
|
|
169
379
|
clearTimeout(timer);
|
|
170
380
|
ws.close();
|
|
171
381
|
reject(new Error(`[dxtrade-api] WebSocket order listener error: ${error.message}`));
|
|
172
382
|
});
|
|
173
383
|
});
|
|
384
|
+
return { promise, ready };
|
|
385
|
+
}
|
|
386
|
+
async function submitOrder(ctx, params) {
|
|
387
|
+
ctx.ensureSession();
|
|
388
|
+
const {
|
|
389
|
+
symbol,
|
|
390
|
+
side,
|
|
391
|
+
quantity,
|
|
392
|
+
orderType,
|
|
393
|
+
orderCode,
|
|
394
|
+
price,
|
|
395
|
+
instrumentId,
|
|
396
|
+
stopLoss,
|
|
397
|
+
takeProfit,
|
|
398
|
+
positionEffect = "OPENING" /* OPENING */,
|
|
399
|
+
positionCode,
|
|
400
|
+
tif = "GTC",
|
|
401
|
+
expireDate,
|
|
402
|
+
metadata
|
|
403
|
+
} = params;
|
|
404
|
+
const info = await getSymbolInfo(ctx, symbol);
|
|
405
|
+
const units = Math.round(quantity * info.lotSize);
|
|
406
|
+
const qty = side === "BUY" /* BUY */ ? units : -units;
|
|
407
|
+
const priceParam = orderType === "STOP" /* STOP */ ? "stopPrice" : "limitPrice";
|
|
408
|
+
const orderData = {
|
|
409
|
+
directExchange: false,
|
|
410
|
+
legs: [
|
|
411
|
+
{
|
|
412
|
+
...instrumentId != null && { instrumentId },
|
|
413
|
+
...positionCode != null && { positionCode },
|
|
414
|
+
positionEffect,
|
|
415
|
+
ratioQuantity: 1,
|
|
416
|
+
symbol
|
|
417
|
+
}
|
|
418
|
+
],
|
|
419
|
+
orderSide: side,
|
|
420
|
+
orderType,
|
|
421
|
+
quantity: qty,
|
|
422
|
+
requestId: orderCode ?? `gwt-uid-931-${crypto.randomUUID()}`,
|
|
423
|
+
timeInForce: tif,
|
|
424
|
+
...expireDate != null && { expireDate },
|
|
425
|
+
...metadata != null && { metadata }
|
|
426
|
+
};
|
|
427
|
+
if (price != null && orderType !== "MARKET" /* MARKET */) {
|
|
428
|
+
orderData[priceParam] = price;
|
|
429
|
+
}
|
|
430
|
+
if (stopLoss) {
|
|
431
|
+
orderData.stopLoss = {
|
|
432
|
+
...stopLoss.offset != null && { fixedOffset: stopLoss.offset },
|
|
433
|
+
...stopLoss.price != null && { fixedPrice: stopLoss.price },
|
|
434
|
+
priceFixed: stopLoss.price != null,
|
|
435
|
+
orderChainId: 0,
|
|
436
|
+
orderId: 0,
|
|
437
|
+
orderType: "STOP" /* STOP */,
|
|
438
|
+
quantityForProtection: qty,
|
|
439
|
+
removed: false
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
if (takeProfit) {
|
|
443
|
+
orderData.takeProfit = {
|
|
444
|
+
...takeProfit.offset != null && { fixedOffset: takeProfit.offset },
|
|
445
|
+
...takeProfit.price != null && { fixedPrice: takeProfit.price },
|
|
446
|
+
priceFixed: takeProfit.price != null,
|
|
447
|
+
orderChainId: 0,
|
|
448
|
+
orderId: 0,
|
|
449
|
+
orderType: "LIMIT" /* LIMIT */,
|
|
450
|
+
quantityForProtection: qty,
|
|
451
|
+
removed: false
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
const wsUrl = endpoints.websocket(ctx.baseUrl);
|
|
456
|
+
const cookieStr = Cookies.serialize(ctx.cookies);
|
|
457
|
+
const listener = createOrderListener(wsUrl, cookieStr, 3e4, ctx.debug);
|
|
458
|
+
await listener.ready;
|
|
459
|
+
const response = await retryRequest(
|
|
460
|
+
{
|
|
461
|
+
method: "POST",
|
|
462
|
+
url: endpoints.submitOrder(ctx.baseUrl),
|
|
463
|
+
data: orderData,
|
|
464
|
+
headers: authHeaders(ctx.csrf, Cookies.serialize(ctx.cookies))
|
|
465
|
+
},
|
|
466
|
+
ctx.retries
|
|
467
|
+
);
|
|
468
|
+
ctx.callbacks.onOrderPlaced?.(response.data);
|
|
469
|
+
const orderUpdate = await listener.promise;
|
|
470
|
+
ctx.callbacks.onOrderUpdate?.(orderUpdate);
|
|
471
|
+
return orderUpdate;
|
|
472
|
+
} catch (error) {
|
|
473
|
+
if (error instanceof DxtradeError) throw error;
|
|
474
|
+
const message = error instanceof Error ? error.response?.data?.message ?? error.message : "Unknown error";
|
|
475
|
+
ctx.throwError("ORDER_ERROR", `Error submitting order: ${message}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/domains/assessments/assessments.ts
|
|
480
|
+
async function getAssessments(ctx, params) {
|
|
481
|
+
ctx.ensureSession();
|
|
482
|
+
try {
|
|
483
|
+
const response = await retryRequest(
|
|
484
|
+
{
|
|
485
|
+
method: "POST",
|
|
486
|
+
url: endpoints.assessments(ctx.baseUrl),
|
|
487
|
+
data: {
|
|
488
|
+
from: params.from,
|
|
489
|
+
instrument: params.instrument,
|
|
490
|
+
subtype: params.subtype ?? null,
|
|
491
|
+
to: params.to
|
|
492
|
+
},
|
|
493
|
+
headers: authHeaders(ctx.csrf, Cookies.serialize(ctx.cookies))
|
|
494
|
+
},
|
|
495
|
+
ctx.retries
|
|
496
|
+
);
|
|
497
|
+
return response.data;
|
|
498
|
+
} catch (error) {
|
|
499
|
+
if (error instanceof DxtradeError) throw error;
|
|
500
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
501
|
+
ctx.throwError("ASSESSMENTS_ERROR", `Error fetching assessments: ${message}`);
|
|
502
|
+
}
|
|
174
503
|
}
|
|
175
504
|
|
|
176
505
|
// src/client.ts
|
|
177
506
|
var DxtradeClient = class {
|
|
178
|
-
|
|
179
|
-
callbacks;
|
|
180
|
-
cookies = {};
|
|
181
|
-
csrf = null;
|
|
182
|
-
baseUrl;
|
|
183
|
-
retries;
|
|
184
|
-
debug;
|
|
507
|
+
_ctx;
|
|
185
508
|
constructor(config) {
|
|
186
|
-
|
|
187
|
-
this.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
509
|
+
const callbacks = config.callbacks ?? {};
|
|
510
|
+
this._ctx = {
|
|
511
|
+
config,
|
|
512
|
+
callbacks,
|
|
513
|
+
cookies: {},
|
|
514
|
+
csrf: null,
|
|
515
|
+
baseUrl: resolveBrokerUrl(config.broker, config.brokerUrls),
|
|
516
|
+
retries: config.retries ?? 3,
|
|
517
|
+
debug: config.debug ?? false,
|
|
518
|
+
ensureSession() {
|
|
519
|
+
if (!this.csrf) {
|
|
520
|
+
throw new DxtradeError("NO_SESSION", "No active session. Call login() and fetchCsrf() or connect() first.");
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
throwError(code, message) {
|
|
524
|
+
const error = new DxtradeError(code, message);
|
|
525
|
+
callbacks.onError?.(error);
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
};
|
|
191
529
|
}
|
|
192
|
-
// ── Session ─────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
193
530
|
async login() {
|
|
194
|
-
|
|
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
|
-
}
|
|
531
|
+
return login(this._ctx);
|
|
221
532
|
}
|
|
222
533
|
async fetchCsrf() {
|
|
223
|
-
|
|
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
|
-
}
|
|
534
|
+
return fetchCsrf(this._ctx);
|
|
244
535
|
}
|
|
245
536
|
async switchAccount(accountId) {
|
|
246
|
-
this.
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
}
|
|
537
|
+
return switchAccount(this._ctx, accountId);
|
|
538
|
+
}
|
|
539
|
+
async connect() {
|
|
540
|
+
return connect(this._ctx);
|
|
265
541
|
}
|
|
266
|
-
// ── Market Data ─────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
267
542
|
async getSymbolSuggestions(text) {
|
|
268
|
-
this.
|
|
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
|
-
}
|
|
543
|
+
return getSymbolSuggestions(this._ctx, text);
|
|
292
544
|
}
|
|
293
545
|
async getSymbolInfo(symbol) {
|
|
294
|
-
this.
|
|
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
|
-
}
|
|
546
|
+
return getSymbolInfo(this._ctx, symbol);
|
|
318
547
|
}
|
|
319
|
-
// ── Trading ─────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
320
548
|
async submitOrder(params) {
|
|
321
|
-
this.
|
|
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
|
-
}
|
|
549
|
+
return submitOrder(this._ctx, params);
|
|
413
550
|
}
|
|
414
|
-
// ── Analytics ───────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
415
551
|
async getAssessments(params) {
|
|
416
|
-
this.
|
|
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
|
-
}
|
|
441
|
-
}
|
|
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
|
-
}
|
|
458
|
-
}
|
|
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
|
-
}
|
|
467
|
-
}
|
|
468
|
-
throwError(code, message) {
|
|
469
|
-
const error = new DxtradeError(code, message);
|
|
470
|
-
this.callbacks.onError?.(error);
|
|
471
|
-
throw error;
|
|
552
|
+
return getAssessments(this._ctx, params);
|
|
472
553
|
}
|
|
473
554
|
};
|
|
474
555
|
export {
|
|
@@ -478,6 +559,8 @@ export {
|
|
|
478
559
|
DxtradeError,
|
|
479
560
|
ORDER_TYPE,
|
|
480
561
|
SIDE,
|
|
562
|
+
TIF,
|
|
563
|
+
WS_MESSAGE,
|
|
481
564
|
endpoints,
|
|
482
565
|
resolveBrokerUrl
|
|
483
566
|
};
|