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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "Prop Futures Algo Trading CLI - Connect to Topstep, Alpha Futures, and other prop firms",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -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 }; // Store for session restore
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, // 0=Active, 3=Inactive
138
+ status: acc.active ? 0 : 3,
146
139
  platform: 'Tradovate',
147
140
  propfirm: this.propfirm.name,
148
- accountType: acc.accountType, // 'Customer' or 'Demo'
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 = this.checkMarketHours();
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 new Promise((resolve, reject) => {
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
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
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
- if (this.ws) {
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
+ };