hedgequantx 1.8.48 → 2.3.0

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 (105) hide show
  1. package/README.md +7 -6
  2. package/bin/cli.js +13 -7
  3. package/dist/algo/copy-engine.js +3 -0
  4. package/dist/algo/copy-engine.jsc +0 -0
  5. package/dist/algo/engine.js +3 -0
  6. package/dist/algo/engine.jsc +0 -0
  7. package/dist/algo/market-data-rithmic.js +3 -0
  8. package/dist/algo/market-data-rithmic.jsc +0 -0
  9. package/dist/algo/market-data.js +3 -0
  10. package/dist/algo/market-data.jsc +0 -0
  11. package/dist/algo/rithmic/connection.js +3 -0
  12. package/dist/algo/rithmic/connection.jsc +0 -0
  13. package/dist/algo/rithmic/constants.js +3 -0
  14. package/dist/algo/rithmic/constants.jsc +0 -0
  15. package/dist/algo/rithmic/index.js +3 -0
  16. package/dist/algo/rithmic/index.jsc +0 -0
  17. package/dist/algo/rithmic/market-data.js +3 -0
  18. package/dist/algo/rithmic/market-data.jsc +0 -0
  19. package/dist/algo/rithmic/pnl.js +3 -0
  20. package/dist/algo/rithmic/pnl.jsc +0 -0
  21. package/dist/algo/rithmic/pool.js +3 -0
  22. package/dist/algo/rithmic/pool.jsc +0 -0
  23. package/dist/algo/rithmic/trading.js +3 -0
  24. package/dist/algo/rithmic/trading.jsc +0 -0
  25. package/dist/algo/rithmic-decoder.js +3 -0
  26. package/dist/algo/rithmic-decoder.jsc +0 -0
  27. package/dist/algo/strategies/ultra-scalping-v2.js +3 -0
  28. package/dist/algo/strategies/ultra-scalping-v2.jsc +0 -0
  29. package/dist/algo/strategies/ultra-scalping.js +3 -0
  30. package/dist/algo/strategies/ultra-scalping.jsc +0 -0
  31. package/dist/algo/trading-api-rithmic.js +3 -0
  32. package/dist/algo/trading-api-rithmic.jsc +0 -0
  33. package/dist/algo/trading-api.js +3 -0
  34. package/dist/algo/trading-api.jsc +0 -0
  35. package/dist/algo/utils/smart-logger.js +3 -0
  36. package/dist/algo/utils/smart-logger.jsc +0 -0
  37. package/dist/algo/utils/smart-logs.js +3 -0
  38. package/dist/algo/utils/smart-logs.jsc +0 -0
  39. package/package.json +33 -10
  40. package/protos/rithmic/account_pnl_position_update.proto +59 -0
  41. package/protos/rithmic/base.proto +7 -0
  42. package/protos/rithmic/best_bid_offer.proto +39 -0
  43. package/protos/rithmic/exchange_order_notification.proto +140 -0
  44. package/protos/rithmic/instrument_pnl_position_update.proto +50 -0
  45. package/protos/rithmic/last_trade.proto +53 -0
  46. package/protos/rithmic/request_account_list.proto +20 -0
  47. package/protos/rithmic/request_cancel_all_orders.proto +15 -0
  48. package/protos/rithmic/request_front_month_contract.proto +10 -0
  49. package/protos/rithmic/request_heartbeat.proto +13 -0
  50. package/protos/rithmic/request_login.proto +28 -0
  51. package/protos/rithmic/request_login_info.proto +10 -0
  52. package/protos/rithmic/request_logout.proto +10 -0
  53. package/protos/rithmic/request_market_data_update.proto +42 -0
  54. package/protos/rithmic/request_new_order.proto +84 -0
  55. package/protos/rithmic/request_pnl_position_snapshot.proto +14 -0
  56. package/protos/rithmic/request_pnl_position_updates.proto +20 -0
  57. package/protos/rithmic/request_product_codes.proto +9 -0
  58. package/protos/rithmic/request_rithmic_system_info.proto +8 -0
  59. package/protos/rithmic/request_show_order_history.proto +16 -0
  60. package/protos/rithmic/request_show_order_history_dates.proto +10 -0
  61. package/protos/rithmic/request_show_order_history_summary.proto +14 -0
  62. package/protos/rithmic/request_show_orders.proto +14 -0
  63. package/protos/rithmic/request_subscribe_for_order_updates.proto +14 -0
  64. package/protos/rithmic/request_tick_bar_replay.proto +48 -0
  65. package/protos/rithmic/request_trade_routes.proto +11 -0
  66. package/protos/rithmic/response_account_list.proto +18 -0
  67. package/protos/rithmic/response_front_month_contract.proto +13 -0
  68. package/protos/rithmic/response_heartbeat.proto +14 -0
  69. package/protos/rithmic/response_login.proto +18 -0
  70. package/protos/rithmic/response_login_info.proto +24 -0
  71. package/protos/rithmic/response_logout.proto +11 -0
  72. package/protos/rithmic/response_market_data_update.proto +9 -0
  73. package/protos/rithmic/response_new_order.proto +18 -0
  74. package/protos/rithmic/response_pnl_position_snapshot.proto +11 -0
  75. package/protos/rithmic/response_pnl_position_updates.proto +11 -0
  76. package/protos/rithmic/response_product_codes.proto +12 -0
  77. package/protos/rithmic/response_rithmic_system_info.proto +12 -0
  78. package/protos/rithmic/response_show_order_history.proto +11 -0
  79. package/protos/rithmic/response_show_order_history_dates.proto +13 -0
  80. package/protos/rithmic/response_show_order_history_summary.proto +11 -0
  81. package/protos/rithmic/response_show_orders.proto +11 -0
  82. package/protos/rithmic/response_subscribe_for_order_updates.proto +11 -0
  83. package/protos/rithmic/response_tick_bar_replay.proto +40 -0
  84. package/protos/rithmic/response_trade_routes.proto +19 -0
  85. package/protos/rithmic/rithmic_order_notification.proto +124 -0
  86. package/src/app.js +136 -89
  87. package/src/config/index.js +27 -8
  88. package/src/config/settings.js +155 -0
  89. package/src/pages/accounts.js +2 -3
  90. package/src/pages/algo/copy-trading.js +293 -200
  91. package/src/pages/algo/one-account.js +1 -1
  92. package/src/security/encryption.js +81 -46
  93. package/src/security/index.js +12 -8
  94. package/src/security/rateLimit.js +68 -65
  95. package/src/security/validation.js +93 -79
  96. package/src/services/hqx-server.js +538 -206
  97. package/src/services/projectx/index.js +327 -204
  98. package/src/services/rithmic/index.js +288 -285
  99. package/src/services/session.js +184 -114
  100. package/src/services/tradovate/index.js +286 -297
  101. package/src/ui/index.js +53 -1
  102. package/src/utils/http.js +236 -0
  103. package/src/utils/index.js +11 -2
  104. package/src/utils/logger.js +64 -33
  105. package/src/utils/prompts.js +79 -71
@@ -6,16 +6,18 @@
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
  const os = require('os');
9
- const { encrypt, decrypt, maskSensitive } = require('../security');
10
- const { ProjectXService } = require('./projectx');
11
- const { RithmicService } = require('./rithmic');
12
- const { TradovateService } = require('./tradovate');
9
+ const crypto = require('crypto');
10
+ const { encrypt, decrypt, maskSensitive, secureWipe } = require('../security/encryption');
11
+ const { SECURITY } = require('../config/settings');
12
+ const { logger } = require('../utils/logger');
13
13
 
14
- const SESSION_DIR = path.join(os.homedir(), '.hedgequantx');
15
- const SESSION_FILE = path.join(SESSION_DIR, 'session.enc');
14
+ const log = logger.scope('Session');
15
+
16
+ const SESSION_DIR = path.join(os.homedir(), SECURITY.SESSION_DIR);
17
+ const SESSION_FILE = path.join(SESSION_DIR, SECURITY.SESSION_FILE);
16
18
 
17
19
  /**
18
- * Secure session storage with AES-256 encryption
20
+ * Secure session storage with AES-256-GCM encryption
19
21
  */
20
22
  const storage = {
21
23
  /**
@@ -24,22 +26,27 @@ const storage = {
24
26
  */
25
27
  _ensureDir() {
26
28
  if (!fs.existsSync(SESSION_DIR)) {
27
- fs.mkdirSync(SESSION_DIR, { recursive: true, mode: 0o700 });
29
+ fs.mkdirSync(SESSION_DIR, { recursive: true, mode: SECURITY.DIR_PERMISSIONS });
30
+ log.debug('Created session directory');
28
31
  }
29
32
  },
30
33
 
31
34
  /**
32
35
  * Saves sessions with encryption
33
36
  * @param {Array} sessions - Sessions to save
37
+ * @returns {boolean} Success status
34
38
  */
35
39
  save(sessions) {
36
40
  try {
37
41
  this._ensureDir();
38
42
  const data = JSON.stringify(sessions);
39
43
  const encrypted = encrypt(data);
40
- fs.writeFileSync(SESSION_FILE, encrypted, { mode: 0o600 });
41
- } catch (e) {
42
- // Silently fail - don't expose errors
44
+ fs.writeFileSync(SESSION_FILE, encrypted, { mode: SECURITY.FILE_PERMISSIONS });
45
+ log.debug('Session saved', { count: sessions.length });
46
+ return true;
47
+ } catch (err) {
48
+ log.error('Failed to save session', { error: err.message });
49
+ return false;
43
50
  }
44
51
  },
45
52
 
@@ -49,34 +56,59 @@ const storage = {
49
56
  */
50
57
  load() {
51
58
  try {
52
- if (fs.existsSync(SESSION_FILE)) {
53
- const encrypted = fs.readFileSync(SESSION_FILE, 'utf8');
54
- const decrypted = decrypt(encrypted);
55
- if (decrypted) {
56
- return JSON.parse(decrypted);
57
- }
59
+ if (!fs.existsSync(SESSION_FILE)) {
60
+ return [];
61
+ }
62
+
63
+ const encrypted = fs.readFileSync(SESSION_FILE, 'utf8');
64
+ const decrypted = decrypt(encrypted);
65
+
66
+ if (!decrypted) {
67
+ log.warn('Session decryption failed - clearing');
68
+ this.clear();
69
+ return [];
58
70
  }
59
- } catch (e) {
60
- // Session corrupted or from different machine - clear it
71
+
72
+ const sessions = JSON.parse(decrypted);
73
+ log.debug('Session loaded', { count: sessions.length });
74
+ return sessions;
75
+ } catch (err) {
76
+ log.error('Failed to load session', { error: err.message });
61
77
  this.clear();
78
+ return [];
62
79
  }
63
- return [];
64
80
  },
65
81
 
66
82
  /**
67
83
  * Securely clears session data
84
+ * @returns {boolean} Success status
68
85
  */
69
86
  clear() {
70
87
  try {
71
88
  if (fs.existsSync(SESSION_FILE)) {
72
89
  // Overwrite with random data before deleting
73
90
  const size = fs.statSync(SESSION_FILE).size;
74
- fs.writeFileSync(SESSION_FILE, require('crypto').randomBytes(size));
91
+ if (size > 0) {
92
+ fs.writeFileSync(SESSION_FILE, crypto.randomBytes(size));
93
+ }
75
94
  fs.unlinkSync(SESSION_FILE);
95
+ log.debug('Session cleared securely');
76
96
  }
77
- } catch (e) {
78
- // Ignore errors
97
+ return true;
98
+ } catch (err) {
99
+ log.error('Failed to clear session', { error: err.message });
100
+ return false;
79
101
  }
102
+ },
103
+ };
104
+
105
+ // Lazy load services to avoid circular dependencies
106
+ let ProjectXService, RithmicService, TradovateService;
107
+ const loadServices = () => {
108
+ if (!ProjectXService) {
109
+ ({ ProjectXService } = require('./projectx'));
110
+ ({ RithmicService } = require('./rithmic'));
111
+ ({ TradovateService } = require('./tradovate'));
80
112
  }
81
113
  };
82
114
 
@@ -84,12 +116,12 @@ const storage = {
84
116
  * Multi-connection manager with secure token handling
85
117
  */
86
118
  const connections = {
87
- /** @type {Array} Active connections */
119
+ /** @type {Array<{type: string, service: Object, propfirm: string, propfirmKey: string, token: string, connectedAt: Date}>} */
88
120
  services: [],
89
121
 
90
122
  /**
91
123
  * Adds a new connection
92
- * @param {string} type - Connection type (projectx, rithmic, etc.)
124
+ * @param {string} type - Connection type (projectx, rithmic, tradovate)
93
125
  * @param {Object} service - Service instance
94
126
  * @param {string} [propfirm] - PropFirm name
95
127
  * @param {string} [token] - Auth token
@@ -99,10 +131,12 @@ const connections = {
99
131
  type,
100
132
  service,
101
133
  propfirm,
134
+ propfirmKey: service.propfirmKey,
102
135
  token: token || service.token,
103
- connectedAt: new Date()
136
+ connectedAt: new Date(),
104
137
  });
105
138
  this.saveToStorage();
139
+ log.info('Connection added', { type, propfirm: propfirm || type });
106
140
  },
107
141
 
108
142
  /**
@@ -119,12 +153,12 @@ const connections = {
119
153
  if (conn.type === 'projectx') {
120
154
  session.token = conn.service.token || conn.token;
121
155
  } else if (conn.type === 'rithmic' || conn.type === 'tradovate') {
122
- // Save encrypted credentials for reconnection
123
156
  session.credentials = conn.service.credentials;
124
157
  }
125
158
 
126
159
  return session;
127
160
  });
161
+
128
162
  storage.save(sessions);
129
163
  },
130
164
 
@@ -133,84 +167,110 @@ const connections = {
133
167
  * @returns {Promise<boolean>} True if sessions were restored
134
168
  */
135
169
  async restoreFromStorage() {
170
+ loadServices();
136
171
  const sessions = storage.load();
137
172
 
173
+ if (!sessions.length) {
174
+ return false;
175
+ }
176
+
177
+ log.info('Restoring sessions', { count: sessions.length });
178
+
138
179
  for (const session of sessions) {
139
180
  try {
140
- if (session.type === 'projectx' && session.token) {
141
- const propfirmKey = session.propfirmKey || session.propfirm.toLowerCase().replace(/ /g, '_');
142
- const service = new ProjectXService(propfirmKey);
143
- service.token = session.token;
144
-
145
- // Validate token is still valid
146
- const userResult = await service.getUser();
147
- if (userResult.success) {
148
- this.services.push({
149
- type: session.type,
150
- service,
151
- propfirm: session.propfirm,
152
- propfirmKey: propfirmKey,
153
- token: session.token,
154
- connectedAt: new Date()
155
- });
156
- }
157
- } else if (session.type === 'rithmic' && session.credentials) {
158
- const propfirmKey = session.propfirmKey || 'apex_rithmic';
159
- const service = new RithmicService(propfirmKey);
160
-
161
- // Try to reconnect
162
- const result = await service.login(session.credentials.username, session.credentials.password);
163
- if (result.success) {
164
- this.services.push({
165
- type: session.type,
166
- service,
167
- propfirm: session.propfirm,
168
- propfirmKey: propfirmKey,
169
- connectedAt: new Date()
170
- });
171
- }
172
- } else if (session.type === 'tradovate' && session.credentials) {
173
- const propfirmKey = session.propfirmKey || 'tradovate';
174
- const service = new TradovateService(propfirmKey);
175
-
176
- // Try to reconnect
177
- const result = await service.login(session.credentials.username, session.credentials.password);
178
- if (result.success) {
179
- this.services.push({
180
- type: session.type,
181
- service,
182
- propfirm: session.propfirm,
183
- propfirmKey: propfirmKey,
184
- connectedAt: new Date()
185
- });
186
- }
187
- }
188
- } catch (e) {
189
- // Invalid session - skip
181
+ await this._restoreSession(session);
182
+ } catch (err) {
183
+ log.warn('Failed to restore session', { type: session.type, error: err.message });
190
184
  }
191
185
  }
192
186
 
193
187
  return this.services.length > 0;
194
188
  },
195
189
 
190
+ /**
191
+ * Restores a single session
192
+ * @private
193
+ */
194
+ async _restoreSession(session) {
195
+ const { type, propfirm, propfirmKey } = session;
196
+
197
+ if (type === 'projectx' && session.token) {
198
+ const service = new ProjectXService(propfirmKey || 'topstep');
199
+ service.token = session.token;
200
+
201
+ const userResult = await service.getUser();
202
+ if (userResult.success) {
203
+ this.services.push({
204
+ type,
205
+ service,
206
+ propfirm,
207
+ propfirmKey,
208
+ token: session.token,
209
+ connectedAt: new Date(),
210
+ });
211
+ log.debug('ProjectX session restored');
212
+ }
213
+ } else if (type === 'rithmic' && session.credentials) {
214
+ const service = new RithmicService(propfirmKey || 'apex_rithmic');
215
+ const result = await service.login(session.credentials.username, session.credentials.password);
216
+
217
+ if (result.success) {
218
+ this.services.push({
219
+ type,
220
+ service,
221
+ propfirm,
222
+ propfirmKey,
223
+ connectedAt: new Date(),
224
+ });
225
+ log.debug('Rithmic session restored');
226
+ }
227
+ } else if (type === 'tradovate' && session.credentials) {
228
+ const service = new TradovateService(propfirmKey || 'tradovate');
229
+ const result = await service.login(session.credentials.username, session.credentials.password);
230
+
231
+ if (result.success) {
232
+ this.services.push({
233
+ type,
234
+ service,
235
+ propfirm,
236
+ propfirmKey,
237
+ connectedAt: new Date(),
238
+ });
239
+ log.debug('Tradovate session restored');
240
+ }
241
+ }
242
+ },
243
+
196
244
  /**
197
245
  * Removes a connection by index
198
246
  * @param {number} index - Connection index
199
247
  */
200
248
  remove(index) {
201
- if (index >= 0 && index < this.services.length) {
202
- const conn = this.services[index];
203
- if (conn.service && conn.service.logout) {
249
+ if (index < 0 || index >= this.services.length) return;
250
+
251
+ const conn = this.services[index];
252
+
253
+ if (conn.service?.logout) {
254
+ try {
204
255
  conn.service.logout();
256
+ } catch (err) {
257
+ log.warn('Logout failed', { error: err.message });
205
258
  }
206
- this.services.splice(index, 1);
207
- this.saveToStorage();
208
259
  }
260
+
261
+ // Clear credentials from memory
262
+ if (conn.service?.credentials) {
263
+ conn.service.credentials = null;
264
+ }
265
+
266
+ this.services.splice(index, 1);
267
+ this.saveToStorage();
268
+ log.info('Connection removed', { type: conn.type });
209
269
  },
210
270
 
211
271
  /**
212
272
  * Gets all connections
213
- * @returns {Array} All connections
273
+ * @returns {Array}
214
274
  */
215
275
  getAll() {
216
276
  return this.services;
@@ -219,7 +279,7 @@ const connections = {
219
279
  /**
220
280
  * Gets connections by type
221
281
  * @param {string} type - Connection type
222
- * @returns {Array} Filtered connections
282
+ * @returns {Array}
223
283
  */
224
284
  getByType(type) {
225
285
  return this.services.filter(c => c.type === type);
@@ -227,7 +287,7 @@ const connections = {
227
287
 
228
288
  /**
229
289
  * Gets connection count
230
- * @returns {number} Number of connections
290
+ * @returns {number}
231
291
  */
232
292
  count() {
233
293
  return this.services.length;
@@ -235,7 +295,7 @@ const connections = {
235
295
 
236
296
  /**
237
297
  * Gets all accounts from all connections
238
- * @returns {Promise<Array>} All accounts
298
+ * @returns {Promise<Array>}
239
299
  */
240
300
  async getAllAccounts() {
241
301
  const allAccounts = [];
@@ -243,18 +303,19 @@ const connections = {
243
303
  for (const conn of this.services) {
244
304
  try {
245
305
  const result = await conn.service.getTradingAccounts();
306
+
246
307
  if (result.success && result.accounts) {
247
- result.accounts.forEach(account => {
308
+ for (const account of result.accounts) {
248
309
  allAccounts.push({
249
310
  ...account,
250
311
  connectionType: conn.type,
251
312
  propfirm: conn.propfirm || conn.type,
252
- service: conn.service
313
+ service: conn.service,
253
314
  });
254
- });
315
+ }
255
316
  }
256
- } catch (e) {
257
- // Skip failed connections
317
+ } catch (err) {
318
+ log.warn('Failed to get accounts', { type: conn.type, error: err.message });
258
319
  }
259
320
  }
260
321
 
@@ -263,31 +324,27 @@ const connections = {
263
324
 
264
325
  /**
265
326
  * Gets the service for a specific account
266
- * @param {string|number} accountId - Account ID to find
267
- * @returns {Object|null} Service instance or null
327
+ * @param {string|number} accountId - Account ID
328
+ * @returns {Object|null}
268
329
  */
269
330
  getServiceForAccount(accountId) {
270
331
  for (const conn of this.services) {
271
- try {
272
- // Check if this service has this account
273
- if (conn.service && conn.service.accounts) {
274
- const found = conn.service.accounts.find(acc =>
275
- acc.accountId == accountId ||
276
- acc.rithmicAccountId == accountId ||
277
- acc.accountName == accountId
278
- );
279
- if (found) return conn.service;
280
- }
281
- } catch (e) {
282
- // Skip
283
- }
332
+ if (!conn.service?.accounts) continue;
333
+
334
+ const found = conn.service.accounts.find(acc =>
335
+ acc.accountId == accountId ||
336
+ acc.rithmicAccountId == accountId ||
337
+ acc.accountName == accountId
338
+ );
339
+
340
+ if (found) return conn.service;
284
341
  }
285
342
  return null;
286
343
  },
287
344
 
288
345
  /**
289
346
  * Checks if any connection is active
290
- * @returns {boolean} True if connected
347
+ * @returns {boolean}
291
348
  */
292
349
  isConnected() {
293
350
  return this.services.length > 0;
@@ -297,27 +354,40 @@ const connections = {
297
354
  * Disconnects all connections and clears sessions
298
355
  */
299
356
  disconnectAll() {
300
- this.services.forEach(conn => {
301
- if (conn.service && conn.service.logout) {
302
- conn.service.logout();
357
+ for (const conn of this.services) {
358
+ try {
359
+ if (conn.service?.logout) {
360
+ conn.service.logout();
361
+ }
362
+ if (conn.service?.disconnect) {
363
+ conn.service.disconnect();
364
+ }
365
+ // Clear credentials
366
+ if (conn.service?.credentials) {
367
+ conn.service.credentials = null;
368
+ }
369
+ } catch (err) {
370
+ log.warn('Disconnect failed', { type: conn.type, error: err.message });
303
371
  }
304
- });
372
+ }
373
+
305
374
  this.services = [];
306
375
  storage.clear();
376
+ log.info('All connections disconnected');
307
377
  },
308
378
 
309
379
  /**
310
380
  * Gets masked connection info for logging
311
- * @returns {Array} Masked connection info
381
+ * @returns {Array}
312
382
  */
313
383
  getInfo() {
314
384
  return this.services.map(conn => ({
315
385
  type: conn.type,
316
386
  propfirm: conn.propfirm,
317
387
  token: maskSensitive(conn.token),
318
- connectedAt: conn.connectedAt
388
+ connectedAt: conn.connectedAt,
319
389
  }));
320
- }
390
+ },
321
391
  };
322
392
 
323
393
  module.exports = { storage, connections };