hedgequantx 1.7.6 → 1.7.8

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.7.6",
3
+ "version": "1.7.8",
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": {
@@ -45,7 +45,6 @@
45
45
  "chalk": "^4.1.2",
46
46
  "commander": "^11.1.0",
47
47
  "figlet": "^1.7.0",
48
- "inquirer": "^7.3.3",
49
48
  "ora": "^5.4.1",
50
49
  "protobufjs": "^8.0.0",
51
50
  "ws": "^8.18.3"
package/src/app.js CHANGED
@@ -4,12 +4,11 @@
4
4
  */
5
5
 
6
6
  const chalk = require('chalk');
7
- const inquirer = require('inquirer');
8
7
  const ora = require('ora');
9
8
 
10
9
  const { connections } = require('./services');
11
10
  const { getLogoWidth, centerText, prepareStdin } = require('./ui');
12
- const { logger } = require('./utils');
11
+ const { logger, prompts } = require('./utils');
13
12
  const { setCachedStats, clearCachedStats } = require('./services/stats-cache');
14
13
 
15
14
  const log = logger.scope('App');
@@ -24,29 +23,21 @@ const { projectXMenu, rithmicMenu, tradovateMenu, addPropAccountMenu, dashboardM
24
23
 
25
24
  // Current service reference
26
25
  let currentService = null;
27
- let currentPlatform = null; // 'projectx' or 'rithmic'
28
26
 
29
27
  /**
30
- * Global terminal restoration - ensures terminal is always restored on exit
28
+ * Global terminal restoration
31
29
  */
32
30
  const restoreTerminal = () => {
33
31
  try {
34
- // Exit alternate screen buffer
35
32
  process.stdout.write('\x1B[?1049l');
36
- // Show cursor
37
33
  process.stdout.write('\x1B[?25h');
38
- // Disable raw mode
39
34
  if (process.stdin.isTTY && process.stdin.isRaw) {
40
35
  process.stdin.setRawMode(false);
41
36
  }
42
- // Remove all keypress listeners
43
37
  process.stdin.removeAllListeners('keypress');
44
- } catch (e) {
45
- // Ignore errors during cleanup
46
- }
38
+ } catch (e) {}
47
39
  };
48
40
 
49
- // Register global handlers to restore terminal on exit/crash
50
41
  process.on('exit', restoreTerminal);
51
42
  process.on('SIGINT', () => { restoreTerminal(); process.exit(0); });
52
43
  process.on('SIGTERM', () => { restoreTerminal(); process.exit(0); });
@@ -62,7 +53,7 @@ process.on('unhandledRejection', (reason) => {
62
53
  });
63
54
 
64
55
  /**
65
- * Refresh cached stats (call after connection/disconnection/add account)
56
+ * Refresh cached stats
66
57
  */
67
58
  const refreshStats = async () => {
68
59
  if (connections.count() > 0) {
@@ -70,20 +61,14 @@ const refreshStats = async () => {
70
61
  const allAccounts = await connections.getAllAccounts();
71
62
  const activeAccounts = allAccounts.filter(acc => acc.status === 0);
72
63
 
73
- // Sum only non-null values from API
74
- let totalBalance = null;
75
- let totalPnl = null;
76
- let hasBalanceData = false;
77
- let hasPnlData = false;
64
+ let totalBalance = null, totalPnl = null;
65
+ let hasBalanceData = false, hasPnlData = false;
78
66
 
79
67
  activeAccounts.forEach(account => {
80
- // Balance: only sum if API returned a value
81
68
  if (account.balance !== null && account.balance !== undefined) {
82
69
  totalBalance = (totalBalance || 0) + account.balance;
83
70
  hasBalanceData = true;
84
71
  }
85
-
86
- // P&L: only sum if API returned a value
87
72
  if (account.profitAndLoss !== null && account.profitAndLoss !== undefined) {
88
73
  totalPnl = (totalPnl || 0) + account.profitAndLoss;
89
74
  hasPnlData = true;
@@ -97,59 +82,36 @@ const refreshStats = async () => {
97
82
  pnl: hasPnlData ? totalPnl : null,
98
83
  pnlPercent: null
99
84
  });
100
- } catch (e) {
101
- // Ignore errors
102
- }
85
+ } catch (e) {}
103
86
  } else {
104
87
  clearCachedStats();
105
88
  }
106
89
  };
107
90
 
108
91
  /**
109
- * Displays the application banner with stats if connected
92
+ * Display banner
110
93
  */
111
94
  const banner = async () => {
112
95
  console.clear();
113
96
  const termWidth = process.stdout.columns || 100;
114
97
  const isMobile = termWidth < 60;
115
- // Logo HEDGEQUANTX + X = 94 chars, need 98 for box (94 + 2 borders + 2 padding)
116
98
  const boxWidth = isMobile ? Math.max(termWidth - 2, 40) : Math.max(getLogoWidth(), 98);
117
99
  const innerWidth = boxWidth - 2;
118
100
  const version = require('../package.json').version;
119
101
 
120
- // Draw logo - compact for mobile, full for desktop
121
-
122
102
  console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
123
103
 
124
104
  if (isMobile) {
125
- // Compact HQX logo for mobile - X in yellow
126
- const logoHQ = [
127
- '██╗ ██╗ ██████╗ ',
128
- '██║ ██║██╔═══██╗',
129
- '███████║██║ ██║',
130
- '██╔══██║██║▄▄ ██║',
131
- '██║ ██║╚██████╔╝',
132
- '╚═╝ ╚═╝ ╚══▀▀═╝ '
133
- ];
134
- const logoX = [
135
- '██╗ ██╗',
136
- '╚██╗██╔╝',
137
- ' ╚███╔╝ ',
138
- ' ██╔██╗ ',
139
- '██╔╝ ██╗',
140
- '╚═╝ ╚═╝'
141
- ];
142
-
105
+ const logoHQ = ['██╗ ██╗ ██████╗ ','██║ ██║██╔═══██╗','███████║██║ ██║','██╔══██║██║▄▄ ██║','██║ ██║╚██████╔╝','╚═╝ ╚═╝ ╚══▀▀═╝ '];
106
+ const logoX = ['██╗ ██╗','╚██╗██╔╝',' ╚███╔╝ ',' ██╔██╗ ','██╔╝ ██╗','╚═╝ ╚═╝'];
143
107
  logoHQ.forEach((line, i) => {
144
108
  const fullLine = chalk.cyan(line) + chalk.yellow(logoX[i]);
145
109
  const totalLen = line.length + logoX[i].length;
146
110
  const padding = innerWidth - totalLen;
147
111
  const leftPad = Math.floor(padding / 2);
148
- const rightPad = padding - leftPad;
149
- console.log(chalk.cyan('║') + ' '.repeat(leftPad) + fullLine + ' '.repeat(rightPad) + chalk.cyan('║'));
112
+ console.log(chalk.cyan('║') + ' '.repeat(leftPad) + fullLine + ' '.repeat(padding - leftPad) + chalk.cyan('║'));
150
113
  });
151
114
  } else {
152
- // Full HEDGEQUANTX logo for desktop
153
115
  const logo = [
154
116
  '██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗',
155
117
  '██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝',
@@ -158,80 +120,58 @@ const banner = async () => {
158
120
  '██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ',
159
121
  '╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ '
160
122
  ];
161
- const logoX = [
162
- '██╗ ██╗',
163
- '╚██╗██╔╝',
164
- ' ╚███╔╝ ',
165
- ' ██╔██╗ ',
166
- '██╔╝ ██╗',
167
- '╚═╝ ╚═╝'
168
- ];
169
-
123
+ const logoX = ['██╗ ██╗','╚██╗██╔╝',' ╚███╔╝ ',' ██╔██╗ ','██╔╝ ██╗','╚═╝ ╚═╝'];
170
124
  logo.forEach((line, i) => {
171
- const mainPart = chalk.cyan(line);
172
- const xPart = chalk.yellow(logoX[i]);
173
- const fullLine = mainPart + xPart;
125
+ const fullLine = chalk.cyan(line) + chalk.yellow(logoX[i]);
174
126
  const totalLen = line.length + logoX[i].length;
175
127
  const padding = innerWidth - totalLen;
176
128
  const leftPad = Math.floor(padding / 2);
177
- const rightPad = padding - leftPad;
178
- console.log(chalk.cyan('║') + ' '.repeat(leftPad) + fullLine + ' '.repeat(rightPad) + chalk.cyan('║'));
129
+ console.log(chalk.cyan('║') + ' '.repeat(leftPad) + fullLine + ' '.repeat(padding - leftPad) + chalk.cyan('║'));
179
130
  });
180
131
  }
181
132
 
182
- // Tagline
183
133
  console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
184
134
  const tagline = isMobile ? `HQX v${version}` : `Prop Futures Algo Trading v${version}`;
185
135
  console.log(chalk.cyan('║') + chalk.white(centerText(tagline, innerWidth)) + chalk.cyan('║'));
186
-
187
- console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
136
+ // No closing line - dashboard will continue the box
188
137
  };
189
138
 
190
139
  /**
191
- * Main connection menu
140
+ * Main menu
192
141
  */
193
142
  const mainMenu = async () => {
194
143
  const boxWidth = getLogoWidth();
195
144
  const innerWidth = boxWidth - 2;
196
145
  const col1Width = Math.floor(innerWidth / 2);
197
- const col2Width = innerWidth - col1Width;
198
146
 
199
- // Connection menu box
200
- console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + ''));
147
+ const menuRow = (left, right) => {
148
+ const leftPlain = left.replace(/\x1b\[[0-9;]*m/g, '');
149
+ const rightPlain = right ? right.replace(/\x1b\[[0-9;]*m/g, '') : '';
150
+ const leftPadded = ' ' + left + ' '.repeat(Math.max(0, col1Width - leftPlain.length - 2));
151
+ const rightPadded = (right || '') + ' '.repeat(Math.max(0, innerWidth - col1Width - rightPlain.length));
152
+ console.log(chalk.cyan('║') + leftPadded + rightPadded + chalk.cyan('║'));
153
+ };
154
+
155
+ // Continue from banner
156
+ console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
201
157
  console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PLATFORM', innerWidth)) + chalk.cyan('║'));
202
158
  console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
203
159
 
204
- // Menu row helper (2 columns)
205
- const menuRow = (left, right) => {
206
- const leftText = ' ' + left;
207
- const rightText = right ? ' ' + right : '';
208
- const leftLen = leftText.replace(/\x1b\[[0-9;]*m/g, '').length;
209
- const rightLen = rightText.replace(/\x1b\[[0-9;]*m/g, '').length;
210
- const leftPad = col1Width - leftLen;
211
- const rightPad = col2Width - rightLen;
212
- console.log(chalk.cyan('║') + leftText + ' '.repeat(Math.max(0, leftPad)) + rightText + ' '.repeat(Math.max(0, rightPad)) + chalk.cyan('║'));
213
- };
160
+ menuRow(chalk.cyan('[1] ProjectX'), chalk.cyan('[2] Rithmic'));
161
+ menuRow(chalk.cyan('[3] Tradovate'), chalk.red('[X] Exit'));
214
162
 
215
163
  console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
216
- console.log();
217
164
 
218
- // Use list type - more stable stdin handling
219
- const { action } = await inquirer.prompt([
220
- {
221
- type: 'list',
222
- name: 'action',
223
- message: chalk.cyan('Select platform:'),
224
- choices: [
225
- { name: chalk.cyan('[1] ProjectX'), value: 'projectx' },
226
- { name: chalk.cyan('[2] Rithmic'), value: 'rithmic' },
227
- { name: chalk.cyan('[3] Tradovate'), value: 'tradovate' },
228
- { name: chalk.red('[X] Exit'), value: 'exit' }
229
- ],
230
- loop: false
231
- }
232
- ]);
165
+ const input = await prompts.textInput('Select (1/2/3/X)');
166
+
167
+ const actionMap = {
168
+ '1': 'projectx',
169
+ '2': 'rithmic',
170
+ '3': 'tradovate',
171
+ 'x': 'exit'
172
+ };
233
173
 
234
- return action;
174
+ return actionMap[(input || '').toLowerCase()] || 'exit';
235
175
  };
236
176
 
237
177
  /**
@@ -242,121 +182,76 @@ const run = async () => {
242
182
  log.info('Starting HQX CLI');
243
183
  await banner();
244
184
 
245
- // Try to restore session
246
- log.debug('Attempting to restore session');
247
185
  const spinner = ora({ text: 'Restoring session...', color: 'yellow' }).start();
248
186
  const restored = await connections.restoreFromStorage();
249
187
 
250
188
  if (restored) {
251
189
  spinner.succeed('Session restored');
252
190
  currentService = connections.getAll()[0].service;
253
- log.info('Session restored', { connections: connections.count() });
254
- // Refresh stats after session restore
255
191
  await refreshStats();
256
192
  } else {
257
193
  spinner.info('No active session');
258
- log.debug('No session to restore');
259
194
  }
260
195
 
261
- // Main loop
262
196
  while (true) {
263
197
  try {
264
- // Ensure stdin is ready for prompts (fixes input leaking to bash)
265
198
  prepareStdin();
266
-
267
- // Display banner (uses cached stats, no refetch)
268
199
  await banner();
269
200
 
270
201
  if (!connections.isConnected()) {
271
202
  const choice = await mainMenu();
272
- log.debug('Main menu choice', { choice });
273
203
 
274
204
  if (choice === 'exit') {
275
- log.info('User exit');
276
205
  console.log(chalk.gray('Goodbye!'));
277
206
  process.exit(0);
278
207
  }
279
208
 
280
- if (choice === 'projectx') {
281
- const service = await projectXMenu();
282
- if (service) {
283
- currentService = service;
284
- await refreshStats(); // Refresh after new connection
285
- }
286
- }
287
-
288
- if (choice === 'rithmic') {
289
- const service = await rithmicMenu();
290
- if (service) {
291
- currentService = service;
292
- await refreshStats(); // Refresh after new connection
293
- }
294
- }
209
+ let service = null;
210
+ if (choice === 'projectx') service = await projectXMenu();
211
+ else if (choice === 'rithmic') service = await rithmicMenu();
212
+ else if (choice === 'tradovate') service = await tradovateMenu();
295
213
 
296
- if (choice === 'tradovate') {
297
- const service = await tradovateMenu();
298
- if (service) {
299
- currentService = service;
300
- await refreshStats(); // Refresh after new connection
301
- }
214
+ if (service) {
215
+ currentService = service;
216
+ await refreshStats();
302
217
  }
303
218
  } else {
304
219
  const action = await dashboardMenu(currentService);
305
- log.debug('Dashboard action', { action });
306
220
 
307
221
  switch (action) {
308
222
  case 'accounts':
309
223
  await showAccounts(currentService);
310
224
  break;
311
-
312
225
  case 'stats':
313
226
  await showStats(currentService);
314
227
  break;
315
228
  case 'add_prop_account':
316
- // Show platform selection menu
317
229
  const platformChoice = await addPropAccountMenu();
318
- if (platformChoice === 'projectx') {
319
- const newService = await projectXMenu();
320
- if (newService) {
321
- currentService = newService;
322
- await refreshStats(); // Refresh after adding account
323
- }
324
- } else if (platformChoice === 'rithmic') {
325
- const newService = await rithmicMenu();
326
- if (newService) {
327
- currentService = newService;
328
- await refreshStats(); // Refresh after adding account
329
- }
330
- } else if (platformChoice === 'tradovate') {
331
- const newService = await tradovateMenu();
332
- if (newService) {
333
- currentService = newService;
334
- await refreshStats(); // Refresh after adding account
335
- }
230
+ let newService = null;
231
+ if (platformChoice === 'projectx') newService = await projectXMenu();
232
+ else if (platformChoice === 'rithmic') newService = await rithmicMenu();
233
+ else if (platformChoice === 'tradovate') newService = await tradovateMenu();
234
+ if (newService) {
235
+ currentService = newService;
236
+ await refreshStats();
336
237
  }
337
238
  break;
338
239
  case 'algotrading':
339
240
  await algoTradingMenu(currentService);
340
241
  break;
341
242
  case 'update':
342
- const updateResult = await handleUpdate();
343
- if (updateResult === 'restart') return; // Stop loop, new process spawned
243
+ await handleUpdate();
344
244
  break;
345
245
  case 'disconnect':
346
- const connCount = connections.count();
347
246
  connections.disconnectAll();
348
247
  currentService = null;
349
248
  clearCachedStats();
350
- console.log(chalk.yellow(`Disconnected ${connCount} connection${connCount > 1 ? 's' : ''}`));
249
+ console.log(chalk.yellow('Disconnected'));
351
250
  break;
352
- case 'exit':
353
- console.log(chalk.gray('Goodbye!'));
354
- process.exit(0);
355
251
  }
356
252
  }
357
253
  } catch (loopError) {
358
- console.error(chalk.red('Error in main loop:'), loopError.message);
359
- // Continue the loop
254
+ console.error(chalk.red('Error:'), loopError.message);
360
255
  }
361
256
  }
362
257
  } catch (error) {