hedgequantx 1.2.34 → 1.2.36

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.
Files changed (51) hide show
  1. package/package.json +2 -1
  2. package/src/app.js +204 -6
  3. package/src/config/propfirms.js +8 -0
  4. package/src/services/rithmic/connection.js +203 -0
  5. package/src/services/rithmic/constants.js +156 -0
  6. package/src/services/rithmic/index.js +487 -0
  7. package/src/services/rithmic/proto/account_pnl_position_update.proto +59 -0
  8. package/src/services/rithmic/proto/base.proto +7 -0
  9. package/src/services/rithmic/proto/best_bid_offer.proto +39 -0
  10. package/src/services/rithmic/proto/exchange_order_notification.proto +140 -0
  11. package/src/services/rithmic/proto/instrument_pnl_position_update.proto +50 -0
  12. package/src/services/rithmic/proto/last_trade.proto +53 -0
  13. package/src/services/rithmic/proto/request_account_list.proto +20 -0
  14. package/src/services/rithmic/proto/request_cancel_all_orders.proto +15 -0
  15. package/src/services/rithmic/proto/request_heartbeat.proto +13 -0
  16. package/src/services/rithmic/proto/request_login.proto +28 -0
  17. package/src/services/rithmic/proto/request_login_info.proto +10 -0
  18. package/src/services/rithmic/proto/request_logout.proto +10 -0
  19. package/src/services/rithmic/proto/request_market_data_update.proto +42 -0
  20. package/src/services/rithmic/proto/request_new_order.proto +84 -0
  21. package/src/services/rithmic/proto/request_pnl_position_snapshot.proto +14 -0
  22. package/src/services/rithmic/proto/request_pnl_position_updates.proto +20 -0
  23. package/src/services/rithmic/proto/request_rithmic_system_info.proto +8 -0
  24. package/src/services/rithmic/proto/request_show_order_history.proto +16 -0
  25. package/src/services/rithmic/proto/request_show_order_history_dates.proto +10 -0
  26. package/src/services/rithmic/proto/request_show_order_history_summary.proto +14 -0
  27. package/src/services/rithmic/proto/request_show_orders.proto +14 -0
  28. package/src/services/rithmic/proto/request_subscribe_for_order_updates.proto +14 -0
  29. package/src/services/rithmic/proto/request_tick_bar_replay.proto +48 -0
  30. package/src/services/rithmic/proto/request_trade_routes.proto +11 -0
  31. package/src/services/rithmic/proto/response_account_list.proto +18 -0
  32. package/src/services/rithmic/proto/response_heartbeat.proto +14 -0
  33. package/src/services/rithmic/proto/response_login.proto +18 -0
  34. package/src/services/rithmic/proto/response_login_info.proto +24 -0
  35. package/src/services/rithmic/proto/response_logout.proto +11 -0
  36. package/src/services/rithmic/proto/response_market_data_update.proto +9 -0
  37. package/src/services/rithmic/proto/response_new_order.proto +18 -0
  38. package/src/services/rithmic/proto/response_pnl_position_snapshot.proto +11 -0
  39. package/src/services/rithmic/proto/response_pnl_position_updates.proto +11 -0
  40. package/src/services/rithmic/proto/response_rithmic_system_info.proto +12 -0
  41. package/src/services/rithmic/proto/response_show_order_history.proto +11 -0
  42. package/src/services/rithmic/proto/response_show_order_history_dates.proto +13 -0
  43. package/src/services/rithmic/proto/response_show_order_history_summary.proto +11 -0
  44. package/src/services/rithmic/proto/response_show_orders.proto +11 -0
  45. package/src/services/rithmic/proto/response_subscribe_for_order_updates.proto +11 -0
  46. package/src/services/rithmic/proto/response_tick_bar_replay.proto +40 -0
  47. package/src/services/rithmic/proto/response_trade_routes.proto +19 -0
  48. package/src/services/rithmic/proto/rithmic_order_notification.proto +124 -0
  49. package/src/services/rithmic/protobuf.js +259 -0
  50. package/src/services/tradovate/constants.js +109 -0
  51. package/src/services/tradovate/index.js +508 -0
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Tradovate Constants
3
+ */
4
+
5
+ // Base URLs
6
+ const TRADOVATE_URLS = {
7
+ // Live Trading
8
+ LIVE_API: 'https://live.tradovateapi.com/v1',
9
+ LIVE_MD_WS: 'wss://md.tradovateapi.com/v1/websocket',
10
+ LIVE_TRADING_WS: 'wss://live.tradovateapi.com/v1/websocket',
11
+
12
+ // Demo/Simulation Trading
13
+ DEMO_API: 'https://demo.tradovateapi.com/v1',
14
+ DEMO_MD_WS: 'wss://md-demo.tradovateapi.com/v1/websocket',
15
+ DEMO_TRADING_WS: 'wss://demo.tradovateapi.com/v1/websocket',
16
+ };
17
+
18
+ // API Paths
19
+ const API_PATHS = {
20
+ // Authentication
21
+ AUTH_TOKEN_REQUEST: '/auth/accesstokenrequest',
22
+ AUTH_RENEW_TOKEN: '/auth/renewaccesstoken',
23
+ AUTH_ME: '/auth/me',
24
+
25
+ // Account
26
+ ACCOUNT_LIST: '/account/list',
27
+ ACCOUNT_FIND: '/account/find',
28
+ ACCOUNT_ITEM: '/account/item',
29
+
30
+ // Cash Balance
31
+ CASH_BALANCE_LIST: '/cashBalance/list',
32
+ CASH_BALANCE_SNAPSHOT: '/cashBalance/getcashbalancesnapshot',
33
+
34
+ // Contract
35
+ CONTRACT_FIND: '/contract/find',
36
+ CONTRACT_ITEM: '/contract/item',
37
+ CONTRACT_SUGGEST: '/contract/suggest',
38
+
39
+ // Product
40
+ PRODUCT_LIST: '/product/list',
41
+ PRODUCT_FIND: '/product/find',
42
+
43
+ // Order
44
+ ORDER_LIST: '/order/list',
45
+ ORDER_PLACE: '/order/placeorder',
46
+ ORDER_MODIFY: '/order/modifyorder',
47
+ ORDER_CANCEL: '/order/cancelorder',
48
+ ORDER_LIQUIDATE_POSITION: '/order/liquidateposition',
49
+
50
+ // Position
51
+ POSITION_LIST: '/position/list',
52
+ POSITION_DEPS: '/position/deps',
53
+
54
+ // Fill
55
+ FILL_LIST: '/fill/list',
56
+ FILL_DEPS: '/fill/deps',
57
+ };
58
+
59
+ // WebSocket Events
60
+ const WS_EVENTS = {
61
+ CONNECTED: 'connected',
62
+ DISCONNECTED: 'disconnected',
63
+ ERROR: 'error',
64
+ QUOTE: 'md/quote',
65
+ DOM: 'md/dom',
66
+ ORDER: 'order',
67
+ FILL: 'fill',
68
+ POSITION: 'position',
69
+ ACCOUNT: 'account',
70
+ CASH_BALANCE: 'cashBalance',
71
+ HEARTBEAT: 'heartbeat',
72
+ };
73
+
74
+ // Token config
75
+ const TOKEN_CONFIG = {
76
+ EXPIRATION_MINUTES: 90,
77
+ RENEW_BEFORE_MINUTES: 15,
78
+ };
79
+
80
+ /**
81
+ * Get base URL for Tradovate API
82
+ */
83
+ function getBaseUrl(isDemo = true) {
84
+ return isDemo ? TRADOVATE_URLS.DEMO_API : TRADOVATE_URLS.LIVE_API;
85
+ }
86
+
87
+ /**
88
+ * Get WebSocket URL for trading
89
+ */
90
+ function getTradingWebSocketUrl(isDemo = true) {
91
+ return isDemo ? TRADOVATE_URLS.DEMO_TRADING_WS : TRADOVATE_URLS.LIVE_TRADING_WS;
92
+ }
93
+
94
+ /**
95
+ * Get WebSocket URL for market data
96
+ */
97
+ function getMdWebSocketUrl(isDemo = true) {
98
+ return isDemo ? TRADOVATE_URLS.DEMO_MD_WS : TRADOVATE_URLS.LIVE_MD_WS;
99
+ }
100
+
101
+ module.exports = {
102
+ TRADOVATE_URLS,
103
+ API_PATHS,
104
+ WS_EVENTS,
105
+ TOKEN_CONFIG,
106
+ getBaseUrl,
107
+ getTradingWebSocketUrl,
108
+ getMdWebSocketUrl,
109
+ };
@@ -0,0 +1,508 @@
1
+ /**
2
+ * Tradovate Service
3
+ * Main service for Tradovate prop firm connections (Apex, TakeProfitTrader)
4
+ */
5
+
6
+ const https = require('https');
7
+ const WebSocket = require('ws');
8
+ const EventEmitter = require('events');
9
+ const { TRADOVATE_URLS, API_PATHS, WS_EVENTS, getBaseUrl, getTradingWebSocketUrl } = require('./constants');
10
+
11
+ class TradovateService extends EventEmitter {
12
+ constructor(propfirmKey) {
13
+ super();
14
+ this.propfirmKey = propfirmKey;
15
+ this.propfirm = this.getPropFirmConfig(propfirmKey);
16
+ this.accessToken = null;
17
+ this.mdAccessToken = null;
18
+ this.userId = null;
19
+ this.tokenExpiration = null;
20
+ this.accounts = [];
21
+ this.user = null;
22
+ this.isDemo = true; // Default to demo
23
+ this.ws = null;
24
+ this.renewalTimer = null;
25
+ }
26
+
27
+ /**
28
+ * Get PropFirm configuration
29
+ */
30
+ getPropFirmConfig(key) {
31
+ const propfirms = {
32
+ 'apex_tradovate': { name: 'Apex (Tradovate)', isDemo: false, defaultBalance: 300000 },
33
+ 'takeprofittrader': { name: 'TakeProfitTrader', isDemo: false, defaultBalance: 150000 },
34
+ 'myfundedfutures': { name: 'MyFundedFutures', isDemo: false, defaultBalance: 150000 },
35
+ };
36
+ return propfirms[key] || { name: key, isDemo: false, defaultBalance: 150000 };
37
+ }
38
+
39
+ /**
40
+ * Login to Tradovate
41
+ * @param {string} username - Tradovate username
42
+ * @param {string} password - Tradovate password
43
+ * @param {object} options - Optional { cid, sec } for API key auth
44
+ */
45
+ async login(username, password, options = {}) {
46
+ try {
47
+ const authData = {
48
+ name: username,
49
+ password: password,
50
+ appId: 'HQX-CLI',
51
+ appVersion: '1.0.0',
52
+ deviceId: this.generateDeviceId(),
53
+ };
54
+
55
+ // Add API key if provided
56
+ if (options.cid) authData.cid = options.cid;
57
+ if (options.sec) authData.sec = options.sec;
58
+
59
+ const result = await this._request(API_PATHS.AUTH_TOKEN_REQUEST, 'POST', authData);
60
+
61
+ if (result.errorText) {
62
+ return { success: false, error: result.errorText };
63
+ }
64
+
65
+ if (!result.accessToken) {
66
+ return { success: false, error: 'No access token received' };
67
+ }
68
+
69
+ this.accessToken = result.accessToken;
70
+ this.mdAccessToken = result.mdAccessToken;
71
+ this.userId = result.userId;
72
+ this.tokenExpiration = new Date(result.expirationTime);
73
+ this.user = { userName: result.name, userId: result.userId };
74
+
75
+ // Setup token renewal
76
+ this.setupTokenRenewal();
77
+
78
+ // Fetch accounts
79
+ await this.fetchAccounts();
80
+
81
+ return { success: true };
82
+
83
+ } catch (error) {
84
+ return { success: false, error: error.message };
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Fetch accounts
90
+ */
91
+ async fetchAccounts() {
92
+ try {
93
+ const accounts = await this._request(API_PATHS.ACCOUNT_LIST, 'GET');
94
+
95
+ if (Array.isArray(accounts)) {
96
+ this.accounts = accounts;
97
+
98
+ // Fetch cash balance for each account
99
+ for (const acc of this.accounts) {
100
+ try {
101
+ const cashBalance = await this._request(
102
+ API_PATHS.CASH_BALANCE_SNAPSHOT,
103
+ 'POST',
104
+ { accountId: acc.id }
105
+ );
106
+ acc.cashBalance = cashBalance;
107
+ } catch (e) {
108
+ acc.cashBalance = null;
109
+ }
110
+ }
111
+ }
112
+
113
+ return this.accounts;
114
+ } catch (error) {
115
+ throw error;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Get trading accounts (formatted for HQX)
121
+ */
122
+ async getTradingAccounts() {
123
+ if (this.accounts.length === 0) {
124
+ await this.fetchAccounts();
125
+ }
126
+
127
+ const tradingAccounts = this.accounts.map((acc) => {
128
+ const cb = acc.cashBalance || {};
129
+ const balance = cb.totalCashValue || cb.netLiquidatingValue || this.propfirm.defaultBalance;
130
+ const startingBalance = this.propfirm.defaultBalance;
131
+ const profitAndLoss = cb.totalPnL || (balance - startingBalance);
132
+ const openPnL = cb.openPnL || 0;
133
+
134
+ return {
135
+ accountId: acc.id,
136
+ tradovateAccountId: acc.id,
137
+ accountName: acc.name,
138
+ name: acc.name,
139
+ balance: balance,
140
+ startingBalance: startingBalance,
141
+ profitAndLoss: profitAndLoss,
142
+ openPnL: openPnL,
143
+ status: acc.active ? 0 : 3, // 0=Active, 3=Inactive
144
+ platform: 'Tradovate',
145
+ propfirm: this.propfirm.name,
146
+ accountType: acc.accountType, // 'Customer' or 'Demo'
147
+ };
148
+ });
149
+
150
+ return { success: true, accounts: tradingAccounts };
151
+ }
152
+
153
+ /**
154
+ * Get positions for an account
155
+ */
156
+ async getPositions(accountId) {
157
+ try {
158
+ const positions = await this._request(API_PATHS.POSITION_DEPS, 'GET', null, { masterid: accountId });
159
+ return positions.filter(p => p.netPos !== 0);
160
+ } catch (error) {
161
+ return [];
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Get fills/trades
167
+ */
168
+ async getFills() {
169
+ try {
170
+ return await this._request(API_PATHS.FILL_LIST, 'GET');
171
+ } catch (error) {
172
+ return [];
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Place an order
178
+ */
179
+ async placeOrder(orderData) {
180
+ try {
181
+ const result = await this._request(API_PATHS.ORDER_PLACE, 'POST', {
182
+ accountId: orderData.accountId,
183
+ action: orderData.side === 0 ? 'Buy' : 'Sell',
184
+ symbol: orderData.symbol,
185
+ orderQty: orderData.size,
186
+ orderType: orderData.type === 2 ? 'Market' : 'Limit',
187
+ price: orderData.price,
188
+ isAutomated: true,
189
+ });
190
+
191
+ if (result.errorText || result.failureReason) {
192
+ return { success: false, error: result.errorText || result.failureText };
193
+ }
194
+
195
+ return { success: true, orderId: result.orderId };
196
+ } catch (error) {
197
+ return { success: false, error: error.message };
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Cancel an order
203
+ */
204
+ async cancelOrder(orderId) {
205
+ try {
206
+ const result = await this._request(API_PATHS.ORDER_CANCEL, 'POST', {
207
+ orderId: orderId,
208
+ isAutomated: true,
209
+ });
210
+
211
+ if (result.errorText) {
212
+ return { success: false, error: result.errorText };
213
+ }
214
+
215
+ return { success: true };
216
+ } catch (error) {
217
+ return { success: false, error: error.message };
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Close a position
223
+ */
224
+ async closePosition(accountId, contractId) {
225
+ try {
226
+ const result = await this._request(API_PATHS.ORDER_LIQUIDATE_POSITION, 'POST', {
227
+ accountId: accountId,
228
+ contractId: contractId,
229
+ isAutomated: true,
230
+ });
231
+
232
+ if (result.errorText) {
233
+ return { success: false, error: result.errorText };
234
+ }
235
+
236
+ return { success: true };
237
+ } catch (error) {
238
+ return { success: false, error: error.message };
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Search contracts
244
+ */
245
+ async searchContracts(text, limit = 10) {
246
+ try {
247
+ return await this._request(API_PATHS.CONTRACT_SUGGEST, 'GET', null, { t: text, l: limit });
248
+ } catch (error) {
249
+ return [];
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Get user info
255
+ */
256
+ async getUser() {
257
+ return this.user;
258
+ }
259
+
260
+ /**
261
+ * Check market hours (same logic as ProjectX)
262
+ */
263
+ checkMarketHours() {
264
+ const now = new Date();
265
+ const utcDay = now.getUTCDay();
266
+ const utcHour = now.getUTCHours();
267
+
268
+ const ctOffset = this.isDST(now) ? 5 : 6;
269
+ const ctHour = (utcHour - ctOffset + 24) % 24;
270
+ const ctDay = utcHour < ctOffset ? (utcDay + 6) % 7 : utcDay;
271
+
272
+ if (ctDay === 6) {
273
+ return { isOpen: false, message: 'Market closed (Saturday)' };
274
+ }
275
+
276
+ if (ctDay === 0 && ctHour < 17) {
277
+ return { isOpen: false, message: 'Market opens Sunday 5:00 PM CT' };
278
+ }
279
+
280
+ if (ctDay === 5 && ctHour >= 16) {
281
+ return { isOpen: false, message: 'Market closed (Friday after 4PM CT)' };
282
+ }
283
+
284
+ if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) {
285
+ return { isOpen: false, message: 'Daily maintenance (4:00-5:00 PM CT)' };
286
+ }
287
+
288
+ return { isOpen: true, message: 'Market is open' };
289
+ }
290
+
291
+ isDST(date) {
292
+ const jan = new Date(date.getFullYear(), 0, 1);
293
+ const jul = new Date(date.getFullYear(), 6, 1);
294
+ const stdOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
295
+ return date.getTimezoneOffset() < stdOffset;
296
+ }
297
+
298
+ /**
299
+ * Setup automatic token renewal
300
+ */
301
+ setupTokenRenewal() {
302
+ if (this.renewalTimer) {
303
+ clearTimeout(this.renewalTimer);
304
+ }
305
+
306
+ // Renew 15 minutes before expiration
307
+ const renewInMs = (90 - 15) * 60 * 1000;
308
+
309
+ this.renewalTimer = setTimeout(async () => {
310
+ try {
311
+ await this.renewToken();
312
+ } catch (error) {
313
+ // Silent fail
314
+ }
315
+ }, renewInMs);
316
+ }
317
+
318
+ /**
319
+ * Renew access token
320
+ */
321
+ async renewToken() {
322
+ if (!this.accessToken) return;
323
+
324
+ try {
325
+ const result = await this._request(API_PATHS.AUTH_RENEW_TOKEN, 'GET');
326
+
327
+ if (result.accessToken) {
328
+ this.accessToken = result.accessToken;
329
+ this.mdAccessToken = result.mdAccessToken;
330
+ this.tokenExpiration = new Date(result.expirationTime);
331
+ this.setupTokenRenewal();
332
+ }
333
+ } catch (error) {
334
+ // Silent fail - will need to re-login
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Connect to WebSocket for real-time updates
340
+ */
341
+ async connectWebSocket() {
342
+ return new Promise((resolve, reject) => {
343
+ const wsUrl = getTradingWebSocketUrl(this.isDemo);
344
+ this.ws = new WebSocket(wsUrl);
345
+
346
+ this.ws.on('open', () => {
347
+ // Authorize
348
+ this.wsSend('authorize', '', { token: this.accessToken });
349
+ resolve(true);
350
+ });
351
+
352
+ this.ws.on('message', (data) => {
353
+ this.handleWsMessage(data);
354
+ });
355
+
356
+ this.ws.on('error', (err) => {
357
+ this.emit('error', err);
358
+ reject(err);
359
+ });
360
+
361
+ this.ws.on('close', () => {
362
+ this.emit('disconnected');
363
+ });
364
+
365
+ setTimeout(() => reject(new Error('WebSocket timeout')), 10000);
366
+ });
367
+ }
368
+
369
+ /**
370
+ * Send WebSocket message
371
+ */
372
+ wsSend(url, query = '', body = null) {
373
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
374
+
375
+ const msg = body
376
+ ? `${url}\n${this.wsRequestId++}\n${query}\n${JSON.stringify(body)}`
377
+ : `${url}\n${this.wsRequestId++}\n${query}\n`;
378
+
379
+ this.ws.send(msg);
380
+ }
381
+
382
+ wsRequestId = 1;
383
+
384
+ /**
385
+ * Handle WebSocket message
386
+ */
387
+ handleWsMessage(data) {
388
+ try {
389
+ const str = data.toString();
390
+
391
+ // Tradovate WS format: frame\nid\ndata
392
+ if (str.startsWith('a')) {
393
+ const json = JSON.parse(str.slice(1));
394
+ if (Array.isArray(json)) {
395
+ json.forEach(msg => this.processWsEvent(msg));
396
+ }
397
+ }
398
+ } catch (e) {
399
+ // Ignore parse errors
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Process WebSocket event
405
+ */
406
+ processWsEvent(msg) {
407
+ if (msg.e === 'props') {
408
+ // User data sync
409
+ if (msg.d?.orders) this.emit(WS_EVENTS.ORDER, msg.d.orders);
410
+ if (msg.d?.positions) this.emit(WS_EVENTS.POSITION, msg.d.positions);
411
+ if (msg.d?.cashBalances) this.emit(WS_EVENTS.CASH_BALANCE, msg.d.cashBalances);
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Disconnect
417
+ */
418
+ async disconnect() {
419
+ if (this.renewalTimer) {
420
+ clearTimeout(this.renewalTimer);
421
+ this.renewalTimer = null;
422
+ }
423
+
424
+ if (this.ws) {
425
+ this.ws.close();
426
+ this.ws = null;
427
+ }
428
+
429
+ this.accessToken = null;
430
+ this.mdAccessToken = null;
431
+ this.accounts = [];
432
+ this.user = null;
433
+ }
434
+
435
+ /**
436
+ * Generate device ID
437
+ */
438
+ generateDeviceId() {
439
+ const crypto = require('crypto');
440
+ const os = require('os');
441
+ const data = `${os.hostname()}-${os.platform()}-${os.arch()}-hqx-cli`;
442
+ return crypto.createHash('md5').update(data).digest('hex');
443
+ }
444
+
445
+ /**
446
+ * HTTP request helper
447
+ */
448
+ _request(path, method = 'GET', body = null, queryParams = null) {
449
+ return new Promise((resolve, reject) => {
450
+ const baseUrl = getBaseUrl(this.isDemo);
451
+ const url = new URL(baseUrl + path);
452
+
453
+ if (queryParams) {
454
+ Object.entries(queryParams).forEach(([key, value]) => {
455
+ url.searchParams.append(key, value);
456
+ });
457
+ }
458
+
459
+ const postData = body ? JSON.stringify(body) : null;
460
+
461
+ const options = {
462
+ hostname: url.hostname,
463
+ port: 443,
464
+ path: url.pathname + url.search,
465
+ method: method,
466
+ headers: {
467
+ 'Content-Type': 'application/json',
468
+ 'Accept': 'application/json',
469
+ },
470
+ };
471
+
472
+ if (postData) {
473
+ options.headers['Content-Length'] = Buffer.byteLength(postData);
474
+ }
475
+
476
+ if (this.accessToken) {
477
+ options.headers['Authorization'] = `Bearer ${this.accessToken}`;
478
+ }
479
+
480
+ const req = https.request(options, (res) => {
481
+ let data = '';
482
+ res.on('data', chunk => data += chunk);
483
+ res.on('end', () => {
484
+ try {
485
+ const json = JSON.parse(data);
486
+ resolve(json);
487
+ } catch (e) {
488
+ resolve(data);
489
+ }
490
+ });
491
+ });
492
+
493
+ req.on('error', reject);
494
+ req.setTimeout(30000, () => {
495
+ req.destroy();
496
+ reject(new Error('Request timeout'));
497
+ });
498
+
499
+ if (postData) {
500
+ req.write(postData);
501
+ }
502
+
503
+ req.end();
504
+ });
505
+ }
506
+ }
507
+
508
+ module.exports = { TradovateService };