hedgequantx 1.7.5 → 1.7.7

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.5",
3
+ "version": "1.7.7",
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": {
@@ -41,11 +41,11 @@
41
41
  "src/"
42
42
  ],
43
43
  "dependencies": {
44
+ "@clack/prompts": "^0.11.0",
44
45
  "asciichart": "^1.5.25",
45
46
  "chalk": "^4.1.2",
46
47
  "commander": "^11.1.0",
47
48
  "figlet": "^1.7.0",
48
- "inquirer": "^7.3.3",
49
49
  "ora": "^5.4.1",
50
50
  "protobufjs": "^8.0.0",
51
51
  "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,42 @@ 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
136
  console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
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
- const col1Width = Math.floor(innerWidth / 2);
197
- const col2Width = innerWidth - col1Width;
198
145
 
199
- // Connection menu box
200
146
  console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
201
147
  console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PLATFORM', innerWidth)) + chalk.cyan('║'));
202
148
  console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
203
-
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
- };
214
-
215
149
  console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
216
- console.log();
217
150
 
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
- }
151
+ const action = await prompts.selectOption('Select platform:', [
152
+ { value: 'projectx', label: '[1] ProjectX' },
153
+ { value: 'rithmic', label: '[2] Rithmic' },
154
+ { value: 'tradovate', label: '[3] Tradovate' },
155
+ { value: 'exit', label: '[X] Exit' }
232
156
  ]);
233
157
 
234
- return action;
158
+ return action || 'exit';
235
159
  };
236
160
 
237
161
  /**
@@ -242,121 +166,76 @@ const run = async () => {
242
166
  log.info('Starting HQX CLI');
243
167
  await banner();
244
168
 
245
- // Try to restore session
246
- log.debug('Attempting to restore session');
247
169
  const spinner = ora({ text: 'Restoring session...', color: 'yellow' }).start();
248
170
  const restored = await connections.restoreFromStorage();
249
171
 
250
172
  if (restored) {
251
173
  spinner.succeed('Session restored');
252
174
  currentService = connections.getAll()[0].service;
253
- log.info('Session restored', { connections: connections.count() });
254
- // Refresh stats after session restore
255
175
  await refreshStats();
256
176
  } else {
257
177
  spinner.info('No active session');
258
- log.debug('No session to restore');
259
178
  }
260
179
 
261
- // Main loop
262
180
  while (true) {
263
181
  try {
264
- // Ensure stdin is ready for prompts (fixes input leaking to bash)
265
182
  prepareStdin();
266
-
267
- // Display banner (uses cached stats, no refetch)
268
183
  await banner();
269
184
 
270
185
  if (!connections.isConnected()) {
271
186
  const choice = await mainMenu();
272
- log.debug('Main menu choice', { choice });
273
187
 
274
188
  if (choice === 'exit') {
275
- log.info('User exit');
276
189
  console.log(chalk.gray('Goodbye!'));
277
190
  process.exit(0);
278
191
  }
279
192
 
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
- }
193
+ let service = null;
194
+ if (choice === 'projectx') service = await projectXMenu();
195
+ else if (choice === 'rithmic') service = await rithmicMenu();
196
+ else if (choice === 'tradovate') service = await tradovateMenu();
295
197
 
296
- if (choice === 'tradovate') {
297
- const service = await tradovateMenu();
298
- if (service) {
299
- currentService = service;
300
- await refreshStats(); // Refresh after new connection
301
- }
198
+ if (service) {
199
+ currentService = service;
200
+ await refreshStats();
302
201
  }
303
202
  } else {
304
203
  const action = await dashboardMenu(currentService);
305
- log.debug('Dashboard action', { action });
306
204
 
307
205
  switch (action) {
308
206
  case 'accounts':
309
207
  await showAccounts(currentService);
310
208
  break;
311
-
312
209
  case 'stats':
313
210
  await showStats(currentService);
314
211
  break;
315
212
  case 'add_prop_account':
316
- // Show platform selection menu
317
213
  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
- }
214
+ let newService = null;
215
+ if (platformChoice === 'projectx') newService = await projectXMenu();
216
+ else if (platformChoice === 'rithmic') newService = await rithmicMenu();
217
+ else if (platformChoice === 'tradovate') newService = await tradovateMenu();
218
+ if (newService) {
219
+ currentService = newService;
220
+ await refreshStats();
336
221
  }
337
222
  break;
338
223
  case 'algotrading':
339
224
  await algoTradingMenu(currentService);
340
225
  break;
341
226
  case 'update':
342
- const updateResult = await handleUpdate();
343
- if (updateResult === 'restart') return; // Stop loop, new process spawned
227
+ await handleUpdate();
344
228
  break;
345
229
  case 'disconnect':
346
- const connCount = connections.count();
347
230
  connections.disconnectAll();
348
231
  currentService = null;
349
232
  clearCachedStats();
350
- console.log(chalk.yellow(`Disconnected ${connCount} connection${connCount > 1 ? 's' : ''}`));
233
+ console.log(chalk.yellow('Disconnected'));
351
234
  break;
352
- case 'exit':
353
- console.log(chalk.gray('Goodbye!'));
354
- process.exit(0);
355
235
  }
356
236
  }
357
237
  } catch (loopError) {
358
- console.error(chalk.red('Error in main loop:'), loopError.message);
359
- // Continue the loop
238
+ console.error(chalk.red('Error:'), loopError.message);
360
239
  }
361
240
  }
362
241
  } catch (error) {