hedgequantx 1.3.1 → 1.3.2
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/package.json
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const https = require('https');
|
|
7
|
-
const WebSocket = require('ws');
|
|
8
7
|
const EventEmitter = require('events');
|
|
9
8
|
const { TRADOVATE_URLS, API_PATHS, WS_EVENTS, getBaseUrl, getTradingWebSocketUrl } = require('./constants');
|
|
9
|
+
const { checkMarketHours, isDST } = require('./market');
|
|
10
|
+
const { connectWebSocket, wsSend, disconnectWebSocket } = require('./websocket');
|
|
10
11
|
|
|
11
12
|
class TradovateService extends EventEmitter {
|
|
12
13
|
constructor(propfirmKey) {
|
|
@@ -21,6 +22,7 @@ class TradovateService extends EventEmitter {
|
|
|
21
22
|
this.user = null;
|
|
22
23
|
this.isDemo = true; // Default to demo
|
|
23
24
|
this.ws = null;
|
|
25
|
+
this.wsRequestId = 1;
|
|
24
26
|
this.renewalTimer = null;
|
|
25
27
|
this.credentials = null; // Store for session restore
|
|
26
28
|
}
|
|
@@ -39,9 +41,6 @@ class TradovateService extends EventEmitter {
|
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
43
|
* Login to Tradovate
|
|
42
|
-
* @param {string} username - Tradovate username
|
|
43
|
-
* @param {string} password - Tradovate password
|
|
44
|
-
* @param {object} options - Optional { cid, sec } for API key auth
|
|
45
44
|
*/
|
|
46
45
|
async login(username, password, options = {}) {
|
|
47
46
|
try {
|
|
@@ -53,7 +52,6 @@ class TradovateService extends EventEmitter {
|
|
|
53
52
|
deviceId: this.generateDeviceId(),
|
|
54
53
|
};
|
|
55
54
|
|
|
56
|
-
// Add API key if provided
|
|
57
55
|
if (options.cid) authData.cid = options.cid;
|
|
58
56
|
if (options.sec) authData.sec = options.sec;
|
|
59
57
|
|
|
@@ -72,16 +70,12 @@ class TradovateService extends EventEmitter {
|
|
|
72
70
|
this.userId = result.userId;
|
|
73
71
|
this.tokenExpiration = new Date(result.expirationTime);
|
|
74
72
|
this.user = { userName: result.name, userId: result.userId };
|
|
75
|
-
this.credentials = { username, password };
|
|
73
|
+
this.credentials = { username, password };
|
|
76
74
|
|
|
77
|
-
// Setup token renewal
|
|
78
75
|
this.setupTokenRenewal();
|
|
79
|
-
|
|
80
|
-
// Fetch accounts
|
|
81
76
|
await this.fetchAccounts();
|
|
82
77
|
|
|
83
78
|
return { success: true };
|
|
84
|
-
|
|
85
79
|
} catch (error) {
|
|
86
80
|
return { success: false, error: error.message };
|
|
87
81
|
}
|
|
@@ -97,7 +91,6 @@ class TradovateService extends EventEmitter {
|
|
|
97
91
|
if (Array.isArray(accounts)) {
|
|
98
92
|
this.accounts = accounts;
|
|
99
93
|
|
|
100
|
-
// Fetch cash balance for each account
|
|
101
94
|
for (const acc of this.accounts) {
|
|
102
95
|
try {
|
|
103
96
|
const cashBalance = await this._request(
|
|
@@ -142,10 +135,10 @@ class TradovateService extends EventEmitter {
|
|
|
142
135
|
startingBalance: startingBalance,
|
|
143
136
|
profitAndLoss: profitAndLoss,
|
|
144
137
|
openPnL: openPnL,
|
|
145
|
-
status: acc.active ? 0 : 3,
|
|
138
|
+
status: acc.active ? 0 : 3,
|
|
146
139
|
platform: 'Tradovate',
|
|
147
140
|
propfirm: this.propfirm.name,
|
|
148
|
-
accountType: acc.accountType,
|
|
141
|
+
accountType: acc.accountType,
|
|
149
142
|
};
|
|
150
143
|
});
|
|
151
144
|
|
|
@@ -263,7 +256,7 @@ class TradovateService extends EventEmitter {
|
|
|
263
256
|
* Get market status
|
|
264
257
|
*/
|
|
265
258
|
async getMarketStatus(accountId) {
|
|
266
|
-
const marketHours =
|
|
259
|
+
const marketHours = checkMarketHours();
|
|
267
260
|
return {
|
|
268
261
|
success: true,
|
|
269
262
|
isOpen: marketHours.isOpen,
|
|
@@ -358,44 +351,6 @@ class TradovateService extends EventEmitter {
|
|
|
358
351
|
}
|
|
359
352
|
}
|
|
360
353
|
|
|
361
|
-
/**
|
|
362
|
-
* Check market hours (same logic as ProjectX)
|
|
363
|
-
*/
|
|
364
|
-
checkMarketHours() {
|
|
365
|
-
const now = new Date();
|
|
366
|
-
const utcDay = now.getUTCDay();
|
|
367
|
-
const utcHour = now.getUTCHours();
|
|
368
|
-
|
|
369
|
-
const ctOffset = this.isDST(now) ? 5 : 6;
|
|
370
|
-
const ctHour = (utcHour - ctOffset + 24) % 24;
|
|
371
|
-
const ctDay = utcHour < ctOffset ? (utcDay + 6) % 7 : utcDay;
|
|
372
|
-
|
|
373
|
-
if (ctDay === 6) {
|
|
374
|
-
return { isOpen: false, message: 'Market closed (Saturday)' };
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (ctDay === 0 && ctHour < 17) {
|
|
378
|
-
return { isOpen: false, message: 'Market opens Sunday 5:00 PM CT' };
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
if (ctDay === 5 && ctHour >= 16) {
|
|
382
|
-
return { isOpen: false, message: 'Market closed (Friday after 4PM CT)' };
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) {
|
|
386
|
-
return { isOpen: false, message: 'Daily maintenance (4:00-5:00 PM CT)' };
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return { isOpen: true, message: 'Market is open' };
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
isDST(date) {
|
|
393
|
-
const jan = new Date(date.getFullYear(), 0, 1);
|
|
394
|
-
const jul = new Date(date.getFullYear(), 6, 1);
|
|
395
|
-
const stdOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
|
|
396
|
-
return date.getTimezoneOffset() < stdOffset;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
354
|
/**
|
|
400
355
|
* Setup automatic token renewal
|
|
401
356
|
*/
|
|
@@ -440,77 +395,14 @@ class TradovateService extends EventEmitter {
|
|
|
440
395
|
* Connect to WebSocket for real-time updates
|
|
441
396
|
*/
|
|
442
397
|
async connectWebSocket() {
|
|
443
|
-
return
|
|
444
|
-
const wsUrl = getTradingWebSocketUrl(this.isDemo);
|
|
445
|
-
this.ws = new WebSocket(wsUrl);
|
|
446
|
-
|
|
447
|
-
this.ws.on('open', () => {
|
|
448
|
-
// Authorize
|
|
449
|
-
this.wsSend('authorize', '', { token: this.accessToken });
|
|
450
|
-
resolve(true);
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
this.ws.on('message', (data) => {
|
|
454
|
-
this.handleWsMessage(data);
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
this.ws.on('error', (err) => {
|
|
458
|
-
this.emit('error', err);
|
|
459
|
-
reject(err);
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
this.ws.on('close', () => {
|
|
463
|
-
this.emit('disconnected');
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
setTimeout(() => reject(new Error('WebSocket timeout')), 10000);
|
|
467
|
-
});
|
|
398
|
+
return connectWebSocket(this);
|
|
468
399
|
}
|
|
469
400
|
|
|
470
401
|
/**
|
|
471
402
|
* Send WebSocket message
|
|
472
403
|
*/
|
|
473
404
|
wsSend(url, query = '', body = null) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const msg = body
|
|
477
|
-
? `${url}\n${this.wsRequestId++}\n${query}\n${JSON.stringify(body)}`
|
|
478
|
-
: `${url}\n${this.wsRequestId++}\n${query}\n`;
|
|
479
|
-
|
|
480
|
-
this.ws.send(msg);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
wsRequestId = 1;
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Handle WebSocket message
|
|
487
|
-
*/
|
|
488
|
-
handleWsMessage(data) {
|
|
489
|
-
try {
|
|
490
|
-
const str = data.toString();
|
|
491
|
-
|
|
492
|
-
// Tradovate WS format: frame\nid\ndata
|
|
493
|
-
if (str.startsWith('a')) {
|
|
494
|
-
const json = JSON.parse(str.slice(1));
|
|
495
|
-
if (Array.isArray(json)) {
|
|
496
|
-
json.forEach(msg => this.processWsEvent(msg));
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
} catch (e) {
|
|
500
|
-
// Ignore parse errors
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* Process WebSocket event
|
|
506
|
-
*/
|
|
507
|
-
processWsEvent(msg) {
|
|
508
|
-
if (msg.e === 'props') {
|
|
509
|
-
// User data sync
|
|
510
|
-
if (msg.d?.orders) this.emit(WS_EVENTS.ORDER, msg.d.orders);
|
|
511
|
-
if (msg.d?.positions) this.emit(WS_EVENTS.POSITION, msg.d.positions);
|
|
512
|
-
if (msg.d?.cashBalances) this.emit(WS_EVENTS.CASH_BALANCE, msg.d.cashBalances);
|
|
513
|
-
}
|
|
405
|
+
return wsSend(this, url, query, body);
|
|
514
406
|
}
|
|
515
407
|
|
|
516
408
|
/**
|
|
@@ -522,10 +414,7 @@ class TradovateService extends EventEmitter {
|
|
|
522
414
|
this.renewalTimer = null;
|
|
523
415
|
}
|
|
524
416
|
|
|
525
|
-
|
|
526
|
-
this.ws.close();
|
|
527
|
-
this.ws = null;
|
|
528
|
-
}
|
|
417
|
+
disconnectWebSocket(this);
|
|
529
418
|
|
|
530
419
|
this.accessToken = null;
|
|
531
420
|
this.mdAccessToken = null;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tradovate Market Hours
|
|
3
|
+
* CME Futures trading hours
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if currently in DST
|
|
8
|
+
*/
|
|
9
|
+
const isDST = (date) => {
|
|
10
|
+
const jan = new Date(date.getFullYear(), 0, 1);
|
|
11
|
+
const jul = new Date(date.getFullYear(), 6, 1);
|
|
12
|
+
const stdOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
|
|
13
|
+
return date.getTimezoneOffset() < stdOffset;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check market hours (CME Futures)
|
|
18
|
+
*/
|
|
19
|
+
const checkMarketHours = () => {
|
|
20
|
+
const now = new Date();
|
|
21
|
+
const utcDay = now.getUTCDay();
|
|
22
|
+
const utcHour = now.getUTCHours();
|
|
23
|
+
|
|
24
|
+
const ctOffset = isDST(now) ? 5 : 6;
|
|
25
|
+
const ctHour = (utcHour - ctOffset + 24) % 24;
|
|
26
|
+
const ctDay = utcHour < ctOffset ? (utcDay + 6) % 7 : utcDay;
|
|
27
|
+
|
|
28
|
+
if (ctDay === 6) {
|
|
29
|
+
return { isOpen: false, message: 'Market closed (Saturday)' };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (ctDay === 0 && ctHour < 17) {
|
|
33
|
+
return { isOpen: false, message: 'Market opens Sunday 5:00 PM CT' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (ctDay === 5 && ctHour >= 16) {
|
|
37
|
+
return { isOpen: false, message: 'Market closed (Friday after 4PM CT)' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) {
|
|
41
|
+
return { isOpen: false, message: 'Daily maintenance (4:00-5:00 PM CT)' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { isOpen: true, message: 'Market is open' };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
module.exports = { checkMarketHours, isDST };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tradovate WebSocket Module
|
|
3
|
+
* Real-time updates via WebSocket
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const WebSocket = require('ws');
|
|
7
|
+
const { getTradingWebSocketUrl, WS_EVENTS } = require('./constants');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create WebSocket connection
|
|
11
|
+
* @param {TradovateService} service - The Tradovate service instance
|
|
12
|
+
*/
|
|
13
|
+
const connectWebSocket = async (service) => {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const wsUrl = getTradingWebSocketUrl(service.isDemo);
|
|
16
|
+
service.ws = new WebSocket(wsUrl);
|
|
17
|
+
service.wsRequestId = 1;
|
|
18
|
+
|
|
19
|
+
service.ws.on('open', () => {
|
|
20
|
+
wsSend(service, 'authorize', '', { token: service.accessToken });
|
|
21
|
+
resolve(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
service.ws.on('message', (data) => {
|
|
25
|
+
handleWsMessage(service, data);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
service.ws.on('error', (err) => {
|
|
29
|
+
service.emit('error', err);
|
|
30
|
+
reject(err);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
service.ws.on('close', () => {
|
|
34
|
+
service.emit('disconnected');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
setTimeout(() => reject(new Error('WebSocket timeout')), 10000);
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Send WebSocket message
|
|
43
|
+
*/
|
|
44
|
+
const wsSend = (service, url, query = '', body = null) => {
|
|
45
|
+
if (!service.ws || service.ws.readyState !== WebSocket.OPEN) return;
|
|
46
|
+
|
|
47
|
+
const msg = body
|
|
48
|
+
? `${url}\n${service.wsRequestId++}\n${query}\n${JSON.stringify(body)}`
|
|
49
|
+
: `${url}\n${service.wsRequestId++}\n${query}\n`;
|
|
50
|
+
|
|
51
|
+
service.ws.send(msg);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Handle WebSocket message
|
|
56
|
+
*/
|
|
57
|
+
const handleWsMessage = (service, data) => {
|
|
58
|
+
try {
|
|
59
|
+
const str = data.toString();
|
|
60
|
+
|
|
61
|
+
if (str.startsWith('a')) {
|
|
62
|
+
const json = JSON.parse(str.slice(1));
|
|
63
|
+
if (Array.isArray(json)) {
|
|
64
|
+
json.forEach(msg => processWsEvent(service, msg));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// Ignore parse errors
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Process WebSocket event
|
|
74
|
+
*/
|
|
75
|
+
const processWsEvent = (service, msg) => {
|
|
76
|
+
if (msg.e === 'props') {
|
|
77
|
+
if (msg.d?.orders) service.emit(WS_EVENTS.ORDER, msg.d.orders);
|
|
78
|
+
if (msg.d?.positions) service.emit(WS_EVENTS.POSITION, msg.d.positions);
|
|
79
|
+
if (msg.d?.cashBalances) service.emit(WS_EVENTS.CASH_BALANCE, msg.d.cashBalances);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Disconnect WebSocket
|
|
85
|
+
*/
|
|
86
|
+
const disconnectWebSocket = (service) => {
|
|
87
|
+
if (service.ws) {
|
|
88
|
+
service.ws.close();
|
|
89
|
+
service.ws = null;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
connectWebSocket,
|
|
95
|
+
wsSend,
|
|
96
|
+
disconnectWebSocket
|
|
97
|
+
};
|