hedgequantx 1.3.0 → 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.
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Rithmic Orders Module
3
+ * Order placement, cancellation, and history
4
+ */
5
+
6
+ const { REQ } = require('./constants');
7
+
8
+ /**
9
+ * Place order via ORDER_PLANT
10
+ * @param {RithmicService} service - The Rithmic service instance
11
+ * @param {Object} orderData - Order parameters
12
+ */
13
+ const placeOrder = async (service, orderData) => {
14
+ if (!service.orderConn || !service.loginInfo) {
15
+ return { success: false, error: 'Not connected' };
16
+ }
17
+
18
+ try {
19
+ service.orderConn.send('RequestNewOrder', {
20
+ templateId: REQ.NEW_ORDER,
21
+ userMsg: ['HQX'],
22
+ fcmId: service.loginInfo.fcmId,
23
+ ibId: service.loginInfo.ibId,
24
+ accountId: orderData.accountId,
25
+ symbol: orderData.symbol,
26
+ exchange: orderData.exchange || 'CME',
27
+ quantity: orderData.size,
28
+ transactionType: orderData.side === 0 ? 1 : 2, // 1=Buy, 2=Sell
29
+ duration: 1, // DAY
30
+ orderType: orderData.type === 2 ? 1 : 2, // 1=Market, 2=Limit
31
+ price: orderData.price || 0,
32
+ });
33
+
34
+ return { success: true, message: 'Order submitted' };
35
+ } catch (error) {
36
+ return { success: false, error: error.message };
37
+ }
38
+ };
39
+
40
+ /**
41
+ * Cancel order
42
+ * @param {RithmicService} service - The Rithmic service instance
43
+ * @param {string} orderId - Order ID to cancel
44
+ */
45
+ const cancelOrder = async (service, orderId) => {
46
+ if (!service.orderConn || !service.loginInfo) {
47
+ return { success: false, error: 'Not connected' };
48
+ }
49
+
50
+ try {
51
+ service.orderConn.send('RequestCancelOrder', {
52
+ templateId: REQ.CANCEL_ORDER,
53
+ userMsg: ['HQX'],
54
+ fcmId: service.loginInfo.fcmId,
55
+ ibId: service.loginInfo.ibId,
56
+ orderId: orderId,
57
+ });
58
+
59
+ return { success: true };
60
+ } catch (error) {
61
+ return { success: false, error: error.message };
62
+ }
63
+ };
64
+
65
+ /**
66
+ * Get active orders
67
+ * @param {RithmicService} service - The Rithmic service instance
68
+ */
69
+ const getOrders = async (service) => {
70
+ if (!service.orderConn || !service.loginInfo) {
71
+ return { success: true, orders: [] };
72
+ }
73
+
74
+ return new Promise((resolve) => {
75
+ const orders = [];
76
+ const timeout = setTimeout(() => {
77
+ resolve({ success: true, orders });
78
+ }, 3000);
79
+
80
+ const orderHandler = (notification) => {
81
+ if (notification.orderId) {
82
+ orders.push({
83
+ orderId: notification.orderId,
84
+ symbol: notification.symbol,
85
+ exchange: notification.exchange,
86
+ side: notification.transactionType === 1 ? 'BUY' : 'SELL',
87
+ quantity: notification.quantity,
88
+ filledQuantity: notification.filledQuantity || 0,
89
+ price: notification.price,
90
+ orderType: notification.orderType,
91
+ status: notification.status,
92
+ });
93
+ }
94
+ };
95
+
96
+ service.once('ordersReceived', () => {
97
+ clearTimeout(timeout);
98
+ service.removeListener('orderNotification', orderHandler);
99
+ resolve({ success: true, orders });
100
+ });
101
+
102
+ service.on('orderNotification', orderHandler);
103
+
104
+ try {
105
+ for (const acc of service.accounts) {
106
+ service.orderConn.send('RequestShowOrders', {
107
+ templateId: REQ.SHOW_ORDERS,
108
+ userMsg: ['HQX'],
109
+ fcmId: acc.fcmId || service.loginInfo.fcmId,
110
+ ibId: acc.ibId || service.loginInfo.ibId,
111
+ accountId: acc.accountId,
112
+ });
113
+ }
114
+ } catch (e) {
115
+ clearTimeout(timeout);
116
+ resolve({ success: false, error: e.message, orders: [] });
117
+ }
118
+ });
119
+ };
120
+
121
+ /**
122
+ * Get order history
123
+ * @param {RithmicService} service - The Rithmic service instance
124
+ * @param {string} date - Date in YYYYMMDD format
125
+ */
126
+ const getOrderHistory = async (service, date) => {
127
+ if (!service.orderConn || !service.loginInfo) {
128
+ return { success: true, orders: [] };
129
+ }
130
+
131
+ const dateStr = date || new Date().toISOString().slice(0, 10).replace(/-/g, '');
132
+
133
+ return new Promise((resolve) => {
134
+ const orders = [];
135
+ const timeout = setTimeout(() => {
136
+ resolve({ success: true, orders });
137
+ }, 3000);
138
+
139
+ try {
140
+ for (const acc of service.accounts) {
141
+ service.orderConn.send('RequestShowOrderHistorySummary', {
142
+ templateId: REQ.SHOW_ORDER_HISTORY,
143
+ userMsg: ['HQX'],
144
+ fcmId: acc.fcmId || service.loginInfo.fcmId,
145
+ ibId: acc.ibId || service.loginInfo.ibId,
146
+ accountId: acc.accountId,
147
+ date: dateStr,
148
+ });
149
+ }
150
+
151
+ setTimeout(() => {
152
+ clearTimeout(timeout);
153
+ resolve({ success: true, orders });
154
+ }, 2000);
155
+ } catch (e) {
156
+ clearTimeout(timeout);
157
+ resolve({ success: false, error: e.message, orders: [] });
158
+ }
159
+ });
160
+ };
161
+
162
+ /**
163
+ * Close position (market order to flatten)
164
+ * @param {RithmicService} service - The Rithmic service instance
165
+ * @param {string} accountId - Account ID
166
+ * @param {string} symbol - Symbol to close
167
+ */
168
+ const closePosition = async (service, accountId, symbol) => {
169
+ const positions = Array.from(service.positions.values());
170
+ const position = positions.find(p => p.accountId === accountId && p.symbol === symbol);
171
+
172
+ if (!position) {
173
+ return { success: false, error: 'Position not found' };
174
+ }
175
+
176
+ return placeOrder(service, {
177
+ accountId,
178
+ symbol,
179
+ exchange: position.exchange,
180
+ size: Math.abs(position.quantity),
181
+ side: position.quantity > 0 ? 1 : 0, // Sell if long, Buy if short
182
+ type: 2, // Market
183
+ });
184
+ };
185
+
186
+ module.exports = {
187
+ placeOrder,
188
+ cancelOrder,
189
+ getOrders,
190
+ getOrderHistory,
191
+ closePosition
192
+ };
@@ -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
+ };