hedgequantx 1.2.146 → 1.2.147

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.2.146",
3
+ "version": "1.2.147",
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": {
package/src/app.js CHANGED
@@ -6,22 +6,18 @@
6
6
  const chalk = require('chalk');
7
7
  const inquirer = require('inquirer');
8
8
  const ora = require('ora');
9
- const figlet = require('figlet');
10
- const { execSync, spawn } = require('child_process');
11
- const path = require('path');
12
9
 
13
- const { ProjectXService, connections } = require('./services');
14
- const { RithmicService } = require('./services/rithmic');
15
- const { TradovateService } = require('./services/tradovate');
16
- const { PROPFIRM_CHOICES, getPropFirmsByPlatform, getPropFirm } = require('./config');
17
- const { getDevice, getSeparator, printLogo, getLogoWidth, drawBoxHeader, drawBoxFooter, centerText, createBoxMenu } = require('./ui');
18
- const { validateUsername, validatePassword, maskSensitive } = require('./security');
10
+ const { connections } = require('./services');
11
+ const { getLogoWidth, centerText } = require('./ui');
19
12
 
20
13
  // Pages
21
14
  const { showStats } = require('./pages/stats');
22
15
  const { showAccounts } = require('./pages/accounts');
23
16
  const { algoTradingMenu } = require('./pages/algo');
24
17
 
18
+ // Menus
19
+ const { projectXMenu, rithmicMenu, tradovateMenu, addPropAccountMenu, dashboardMenu, handleUpdate } = require('./menus');
20
+
25
21
  // Current service reference
26
22
  let currentService = null;
27
23
  let currentPlatform = null; // 'projectx' or 'rithmic'
@@ -87,7 +83,10 @@ process.on('unhandledRejection', (reason) => {
87
83
  */
88
84
  const banner = async () => {
89
85
  console.clear();
90
- const boxWidth = getLogoWidth();
86
+ const termWidth = process.stdout.columns || 100;
87
+ const isMobile = termWidth < 60;
88
+ // Logo HEDGEQUANTX + X = 94 chars, need 98 for box (94 + 2 borders + 2 padding)
89
+ const boxWidth = isMobile ? Math.max(termWidth - 2, 40) : Math.max(getLogoWidth(), 98);
91
90
  const innerWidth = boxWidth - 2;
92
91
  const version = require('../package.json').version;
93
92
 
@@ -123,8 +122,6 @@ const banner = async () => {
123
122
  }
124
123
 
125
124
  // Draw logo - compact for mobile, full for desktop
126
- const termWidth = process.stdout.columns || 80;
127
- const isMobile = termWidth < 60;
128
125
 
129
126
  console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
130
127
 
@@ -222,391 +219,6 @@ const banner = async () => {
222
219
  console.log();
223
220
  };
224
221
 
225
- /**
226
- * Login prompt with validation
227
- * @param {string} propfirmName - PropFirm display name
228
- * @returns {Promise<{username: string, password: string}>}
229
- */
230
- const loginPrompt = async (propfirmName) => {
231
- const device = getDevice();
232
- console.log();
233
- console.log(chalk.cyan(`Connecting to ${propfirmName}...`));
234
- console.log();
235
-
236
- const credentials = await inquirer.prompt([
237
- {
238
- type: 'input',
239
- name: 'username',
240
- message: chalk.white.bold('Username:'),
241
- validate: (input) => {
242
- try {
243
- validateUsername(input);
244
- return true;
245
- } catch (e) {
246
- return e.message;
247
- }
248
- }
249
- },
250
- {
251
- type: 'password',
252
- name: 'password',
253
- message: chalk.white.bold('Password:'),
254
- mask: '*',
255
- validate: (input) => {
256
- try {
257
- validatePassword(input);
258
- return true;
259
- } catch (e) {
260
- return e.message;
261
- }
262
- }
263
- }
264
- ]);
265
-
266
- return credentials;
267
- };
268
-
269
- /**
270
- * ProjectX platform connection menu
271
- */
272
- const projectXMenu = async () => {
273
- const propfirms = getPropFirmsByPlatform('ProjectX');
274
- const boxWidth = getLogoWidth();
275
- const innerWidth = boxWidth - 2;
276
- const numCols = 3;
277
- const colWidth = Math.floor(innerWidth / numCols);
278
-
279
- // Build numbered list
280
- const numbered = propfirms.map((pf, i) => ({
281
- num: i + 1,
282
- key: pf.key,
283
- name: pf.displayName
284
- }));
285
-
286
- // PropFirm selection box
287
- console.log();
288
- console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
289
- console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM', innerWidth)) + chalk.cyan('║'));
290
- console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
291
-
292
- // Display in 3 columns with fixed width alignment
293
- const rows = Math.ceil(numbered.length / numCols);
294
- const maxNum = numbered.length;
295
- const numWidth = maxNum >= 10 ? 4 : 3; // [XX] or [X]
296
-
297
- for (let row = 0; row < rows; row++) {
298
- let line = '';
299
- for (let col = 0; col < numCols; col++) {
300
- const idx = row + col * rows;
301
- if (idx < numbered.length) {
302
- const item = numbered[idx];
303
- const numStr = item.num.toString().padStart(2, ' ');
304
- const coloredText = chalk.cyan(`[${numStr}]`) + ' ' + chalk.white(item.name);
305
- const textLen = 4 + 1 + item.name.length; // [XX] + space + name
306
- const padding = colWidth - textLen - 2;
307
- line += ' ' + coloredText + ' '.repeat(Math.max(0, padding));
308
- } else {
309
- line += ' '.repeat(colWidth);
310
- }
311
- }
312
- // Adjust for exact width
313
- const lineLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
314
- const adjust = innerWidth - lineLen;
315
- console.log(chalk.cyan('║') + line + ' '.repeat(Math.max(0, adjust)) + chalk.cyan('║'));
316
- }
317
-
318
- console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
319
- const backText = ' ' + chalk.red('[X] Back');
320
- const backLen = '[X] Back'.length + 2;
321
- console.log(chalk.cyan('║') + backText + ' '.repeat(innerWidth - backLen) + chalk.cyan('║'));
322
- console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
323
- console.log();
324
-
325
- const validInputs = numbered.map(n => n.num.toString());
326
- validInputs.push('x', 'X');
327
-
328
- const { action } = await inquirer.prompt([
329
- {
330
- type: 'input',
331
- name: 'action',
332
- message: chalk.cyan(`Enter choice (1-${numbered.length}/X):`),
333
- validate: (input) => {
334
- if (validInputs.includes(input)) return true;
335
- return `Please enter 1-${numbered.length} or X`;
336
- }
337
- }
338
- ]);
339
-
340
- if (action.toLowerCase() === 'x') return null;
341
-
342
- const selectedIdx = parseInt(action) - 1;
343
- const selectedPropfirm = numbered[selectedIdx];
344
-
345
- const credentials = await loginPrompt(selectedPropfirm.name);
346
- const spinner = ora('Authenticating...').start();
347
-
348
- try {
349
- const service = new ProjectXService(selectedPropfirm.key);
350
- const result = await service.login(credentials.username, credentials.password);
351
-
352
- if (result.success) {
353
- await service.getUser();
354
- connections.add('projectx', service, service.propfirm.name);
355
- currentService = service;
356
- currentPlatform = 'projectx';
357
- spinner.succeed(`Connected to ${service.propfirm.name}`);
358
- return service;
359
- } else {
360
- spinner.fail(result.error || 'Authentication failed');
361
- return null;
362
- }
363
- } catch (error) {
364
- spinner.fail(error.message);
365
- return null;
366
- }
367
- };
368
-
369
- /**
370
- * Rithmic platform connection menu
371
- */
372
- const rithmicMenu = async () => {
373
- const propfirms = getPropFirmsByPlatform('Rithmic');
374
- const boxWidth = getLogoWidth();
375
- const innerWidth = boxWidth - 2;
376
- const numCols = 3;
377
- const colWidth = Math.floor(innerWidth / numCols);
378
-
379
- // Build numbered list
380
- const numbered = propfirms.map((pf, i) => ({
381
- num: i + 1,
382
- key: pf.key,
383
- name: pf.displayName,
384
- systemName: pf.rithmicSystem
385
- }));
386
-
387
- // PropFirm selection box
388
- console.log();
389
- console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
390
- console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM (RITHMIC)', innerWidth)) + chalk.cyan('║'));
391
- console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
392
-
393
- // Display in 3 columns with fixed width alignment
394
- const rows = Math.ceil(numbered.length / numCols);
395
-
396
- for (let row = 0; row < rows; row++) {
397
- let line = '';
398
- for (let col = 0; col < numCols; col++) {
399
- const idx = row + col * rows;
400
- if (idx < numbered.length) {
401
- const item = numbered[idx];
402
- const numStr = item.num.toString().padStart(2, ' ');
403
- const coloredText = chalk.cyan(`[${numStr}]`) + ' ' + chalk.white(item.name);
404
- const textLen = 4 + 1 + item.name.length;
405
- const padding = colWidth - textLen - 2;
406
- line += ' ' + coloredText + ' '.repeat(Math.max(0, padding));
407
- } else {
408
- line += ' '.repeat(colWidth);
409
- }
410
- }
411
- const lineLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
412
- const adjust = innerWidth - lineLen;
413
- console.log(chalk.cyan('║') + line + ' '.repeat(Math.max(0, adjust)) + chalk.cyan('║'));
414
- }
415
-
416
- console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
417
- const backText = ' ' + chalk.red('[X] Back');
418
- const backLen = '[X] Back'.length + 2;
419
- console.log(chalk.cyan('║') + backText + ' '.repeat(innerWidth - backLen) + chalk.cyan('║'));
420
- console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
421
- console.log();
422
-
423
- const validInputs = numbered.map(n => n.num.toString());
424
- validInputs.push('x', 'X');
425
-
426
- const { action } = await inquirer.prompt([
427
- {
428
- type: 'input',
429
- name: 'action',
430
- message: chalk.cyan(`Enter choice (1-${numbered.length}/X):`),
431
- validate: (input) => {
432
- if (validInputs.includes(input)) return true;
433
- return `Please enter 1-${numbered.length} or X`;
434
- }
435
- }
436
- ]);
437
-
438
- if (action.toLowerCase() === 'x') return null;
439
-
440
- const selectedIdx = parseInt(action) - 1;
441
- const selectedPropfirm = numbered[selectedIdx];
442
-
443
- const credentials = await loginPrompt(selectedPropfirm.name);
444
- const spinner = ora('Connecting to Rithmic...').start();
445
-
446
- try {
447
- const service = new RithmicService(selectedPropfirm.key);
448
- const result = await service.login(credentials.username, credentials.password);
449
-
450
- if (result.success) {
451
- spinner.text = 'Fetching accounts...';
452
- const accResult = await service.getTradingAccounts();
453
-
454
- connections.add('rithmic', service, service.propfirm.name);
455
- currentService = service;
456
- currentPlatform = 'rithmic';
457
- spinner.succeed(`Connected to ${service.propfirm.name} (${accResult.accounts?.length || 0} accounts)`);
458
-
459
- // Small pause to see the success message
460
- await new Promise(r => setTimeout(r, 1500));
461
- return service;
462
- } else {
463
- spinner.fail(result.error || 'Authentication failed');
464
- await new Promise(r => setTimeout(r, 2000));
465
- return null;
466
- }
467
- } catch (error) {
468
- spinner.fail(`Connection error: ${error.message}`);
469
- await new Promise(r => setTimeout(r, 2000));
470
- return null;
471
- }
472
- };
473
-
474
- /**
475
- * Tradovate platform connection menu
476
- */
477
- const tradovateMenu = async () => {
478
- const propfirms = getPropFirmsByPlatform('Tradovate');
479
- const boxWidth = getLogoWidth();
480
- const innerWidth = boxWidth - 2;
481
-
482
- // Build numbered list
483
- const numbered = propfirms.map((pf, i) => ({
484
- num: i + 1,
485
- key: pf.key,
486
- name: pf.displayName
487
- }));
488
-
489
- // PropFirm selection box
490
- console.log();
491
- console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
492
- console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM (TRADOVATE)', innerWidth)) + chalk.cyan('║'));
493
- console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
494
-
495
- // Display propfirms
496
- for (const item of numbered) {
497
- const numStr = item.num.toString().padStart(2, ' ');
498
- const text = ' ' + chalk.cyan(`[${numStr}]`) + ' ' + chalk.white(item.name);
499
- const textLen = 4 + 1 + item.name.length + 2;
500
- console.log(chalk.cyan('║') + text + ' '.repeat(innerWidth - textLen) + chalk.cyan('║'));
501
- }
502
-
503
- console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
504
- const backText = ' ' + chalk.red('[X] Back');
505
- const backLen = '[X] Back'.length + 2;
506
- console.log(chalk.cyan('║') + backText + ' '.repeat(innerWidth - backLen) + chalk.cyan('║'));
507
- console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
508
- console.log();
509
-
510
- const validInputs = numbered.map(n => n.num.toString());
511
- validInputs.push('x', 'X');
512
-
513
- const { action } = await inquirer.prompt([
514
- {
515
- type: 'input',
516
- name: 'action',
517
- message: chalk.cyan(`Enter choice (1-${numbered.length}/X):`),
518
- validate: (input) => {
519
- if (validInputs.includes(input)) return true;
520
- return `Please enter 1-${numbered.length} or X`;
521
- }
522
- }
523
- ]);
524
-
525
- if (action.toLowerCase() === 'x') return null;
526
-
527
- const selectedIdx = parseInt(action) - 1;
528
- const selectedPropfirm = numbered[selectedIdx];
529
-
530
- const credentials = await loginPrompt(selectedPropfirm.name);
531
- const spinner = ora('Connecting to Tradovate...').start();
532
-
533
- try {
534
- const service = new TradovateService(selectedPropfirm.key);
535
- const result = await service.login(credentials.username, credentials.password);
536
-
537
- if (result.success) {
538
- spinner.text = 'Fetching accounts...';
539
- await service.getTradingAccounts();
540
-
541
- connections.add('tradovate', service, service.propfirm.name);
542
- currentService = service;
543
- currentPlatform = 'tradovate';
544
- spinner.succeed(`Connected to ${service.propfirm.name}`);
545
- return service;
546
- } else {
547
- spinner.fail(result.error || 'Authentication failed');
548
- return null;
549
- }
550
- } catch (error) {
551
- spinner.fail(error.message);
552
- return null;
553
- }
554
- };
555
-
556
- /**
557
- * Add Prop Account menu (select platform)
558
- */
559
- const addPropAccountMenu = async () => {
560
- const boxWidth = getLogoWidth();
561
- const innerWidth = boxWidth - 2;
562
- const col1Width = Math.floor(innerWidth / 2);
563
- const col2Width = innerWidth - col1Width;
564
-
565
- console.log();
566
- console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
567
- console.log(chalk.cyan('║') + chalk.white.bold(centerText('ADD PROP ACCOUNT', innerWidth)) + chalk.cyan('║'));
568
- console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
569
-
570
- const menuRow = (left, right) => {
571
- const leftText = ' ' + left;
572
- const rightText = right ? ' ' + right : '';
573
- const leftLen = leftText.replace(/\x1b\[[0-9;]*m/g, '').length;
574
- const rightLen = rightText.replace(/\x1b\[[0-9;]*m/g, '').length;
575
- const leftPad = col1Width - leftLen;
576
- const rightPad = col2Width - rightLen;
577
- console.log(chalk.cyan('║') + leftText + ' '.repeat(Math.max(0, leftPad)) + rightText + ' '.repeat(Math.max(0, rightPad)) + chalk.cyan('║'));
578
- };
579
-
580
- menuRow(chalk.cyan('[1] ProjectX'), chalk.cyan('[2] Rithmic'));
581
- menuRow(chalk.cyan('[3] Tradovate'), chalk.red('[X] Back'));
582
-
583
- console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
584
- console.log();
585
-
586
- const { action } = await inquirer.prompt([
587
- {
588
- type: 'input',
589
- name: 'action',
590
- message: chalk.cyan('Enter choice (1/2/3/X):'),
591
- validate: (input) => {
592
- const valid = ['1', '2', '3', 'x', 'X'];
593
- if (valid.includes(input)) return true;
594
- return 'Please enter 1, 2, 3 or X';
595
- }
596
- }
597
- ]);
598
-
599
- const actionMap = {
600
- '1': 'projectx',
601
- '2': 'rithmic',
602
- '3': 'tradovate',
603
- 'x': null,
604
- 'X': null
605
- };
606
-
607
- return actionMap[action];
608
- };
609
-
610
222
  /**
611
223
  * Main connection menu
612
224
  */
@@ -663,205 +275,6 @@ const mainMenu = async () => {
663
275
  return actionMap[action] || 'exit';
664
276
  };
665
277
 
666
- /**
667
- * Dashboard menu after login
668
- * @param {Object} service - Connected service
669
- */
670
- const dashboardMenu = async (service) => {
671
- const user = service.user;
672
- const boxWidth = getLogoWidth();
673
- const W = boxWidth - 2; // Same width as logo (inner width)
674
-
675
- // Helper to center text
676
- const centerLine = (text, width) => {
677
- const pad = Math.floor((width - text.length) / 2);
678
- return ' '.repeat(Math.max(0, pad)) + text + ' '.repeat(Math.max(0, width - pad - text.length));
679
- };
680
-
681
- // Helper to pad text left
682
- const padLine = (text, width) => {
683
- return ' ' + text + ' '.repeat(Math.max(0, width - text.length - 1));
684
- };
685
-
686
- // Dashboard box header
687
- console.log();
688
- console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
689
- console.log(chalk.cyan('║') + chalk.yellow.bold(centerLine('Welcome, HQX Trader!', W)) + chalk.cyan('║'));
690
- console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
691
-
692
- // Connection info - show all active connections in boxes (max 3 per row)
693
- const allConns = connections.getAll();
694
- if (allConns.length > 0) {
695
- const maxPerRow = 3;
696
- const boxPadding = 2; // padding inside each mini-box
697
- const gap = 2; // gap between boxes
698
-
699
- // Calculate box width based on number of connections (max 3)
700
- const numBoxes = Math.min(allConns.length, maxPerRow);
701
- const totalGaps = (numBoxes - 1) * gap;
702
- const boxWidth = Math.floor((W - totalGaps - 2) / numBoxes); // -2 for outer padding
703
-
704
- // Process connections in rows of 3
705
- for (let rowStart = 0; rowStart < allConns.length; rowStart += maxPerRow) {
706
- const rowConns = allConns.slice(rowStart, rowStart + maxPerRow);
707
- const numInRow = rowConns.length;
708
- const rowBoxWidth = Math.floor((W - (numInRow - 1) * gap - 2) / numInRow);
709
-
710
- // Top border of boxes
711
- let topLine = ' ';
712
- for (let i = 0; i < numInRow; i++) {
713
- topLine += '┌' + '─'.repeat(rowBoxWidth - 2) + '┐';
714
- if (i < numInRow - 1) topLine += ' '.repeat(gap);
715
- }
716
- const topPad = W - topLine.length;
717
- console.log(chalk.cyan('║') + chalk.green(topLine) + ' '.repeat(Math.max(0, topPad)) + chalk.cyan('║'));
718
-
719
- // Content of boxes
720
- let contentLine = ' ';
721
- for (let i = 0; i < numInRow; i++) {
722
- const connText = rowConns[i].propfirm || rowConns[i].type || 'Connected';
723
- const truncated = connText.length > rowBoxWidth - 4 ? connText.slice(0, rowBoxWidth - 7) + '...' : connText;
724
- const innerWidth = rowBoxWidth - 4; // -2 for borders, -2 for padding
725
- const textPad = Math.floor((innerWidth - truncated.length) / 2);
726
- const textPadRight = innerWidth - truncated.length - textPad;
727
- contentLine += '│ ' + ' '.repeat(textPad) + truncated + ' '.repeat(textPadRight) + ' │';
728
- if (i < numInRow - 1) contentLine += ' '.repeat(gap);
729
- }
730
- const contentPad = W - contentLine.length;
731
- console.log(chalk.cyan('║') + chalk.green(contentLine) + ' '.repeat(Math.max(0, contentPad)) + chalk.cyan('║'));
732
-
733
- // Bottom border of boxes
734
- let bottomLine = ' ';
735
- for (let i = 0; i < numInRow; i++) {
736
- bottomLine += '└' + '─'.repeat(rowBoxWidth - 2) + '┘';
737
- if (i < numInRow - 1) bottomLine += ' '.repeat(gap);
738
- }
739
- const bottomPad = W - bottomLine.length;
740
- console.log(chalk.cyan('║') + chalk.green(bottomLine) + ' '.repeat(Math.max(0, bottomPad)) + chalk.cyan('║'));
741
- }
742
- }
743
-
744
- console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
745
-
746
- // Menu options in 2 columns
747
- const col1Width = Math.floor(W / 2);
748
- const col2Width = W - col1Width;
749
-
750
- const menuRow = (left, right) => {
751
- const leftPlain = left.replace(/\x1b\[[0-9;]*m/g, '');
752
- const rightPlain = right ? right.replace(/\x1b\[[0-9;]*m/g, '') : '';
753
- const leftPad = ' '.repeat(Math.max(0, col1Width - leftPlain.length - 2));
754
- const rightPad = ' '.repeat(Math.max(0, col2Width - rightPlain.length - 2));
755
- console.log(chalk.cyan('║') + ' ' + left + leftPad + ' ' + (right || '') + rightPad + chalk.cyan('║'));
756
- };
757
-
758
- menuRow(chalk.cyan('[1] View Accounts'), chalk.cyan('[2] View Stats'));
759
- menuRow(chalk.cyan('[+] Add Prop-Account'), chalk.cyan('[A] Algo-Trading'));
760
- menuRow(chalk.yellow('[U] Update HQX'), chalk.red('[X] Disconnect'));
761
-
762
- console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
763
- console.log();
764
-
765
- const { action } = await inquirer.prompt([
766
- {
767
- type: 'input',
768
- name: 'action',
769
- message: chalk.cyan('Enter choice (1/2/+/A/U/X):'),
770
- validate: (input) => {
771
- const valid = ['1', '2', '+', 'a', 'A', 'u', 'U', 'x', 'X'];
772
- if (valid.includes(input)) return true;
773
- return 'Please enter a valid option';
774
- }
775
- }
776
- ]);
777
-
778
- // Map input to action
779
- const actionMap = {
780
- '1': 'accounts',
781
- '2': 'stats',
782
- '+': 'add_prop_account',
783
- 'a': 'algotrading',
784
- 'A': 'algotrading',
785
- 'u': 'update',
786
- 'U': 'update',
787
- 'x': 'disconnect',
788
- 'X': 'disconnect'
789
- };
790
-
791
- return actionMap[action] || 'accounts';
792
- };
793
-
794
- /**
795
- * Handles the update process with auto-restart
796
- */
797
- const handleUpdate = async () => {
798
- const { execSync: exec } = require('child_process');
799
- const pkg = require('../package.json');
800
- const currentVersion = pkg.version;
801
- const spinner = ora('Checking for updates...').start();
802
-
803
- try {
804
- // Check latest version on npm
805
- spinner.text = 'Checking npm registry...';
806
- let latestVersion;
807
- try {
808
- latestVersion = exec('npm view hedgequantx version', { stdio: 'pipe' }).toString().trim();
809
- } catch (e) {
810
- spinner.fail('Cannot reach npm registry');
811
- console.log();
812
- await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
813
- return;
814
- }
815
-
816
- if (currentVersion === latestVersion) {
817
- spinner.succeed('Already up to date!');
818
- console.log();
819
- console.log(chalk.green(` ✓ You have the latest version of HedgeQuantX CLI: v${currentVersion}`));
820
- console.log();
821
- await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
822
- return;
823
- }
824
-
825
- // Update via npm
826
- spinner.text = `Updating v${currentVersion} -> v${latestVersion}...`;
827
- try {
828
- exec('npm install -g hedgequantx@latest', { stdio: 'pipe' });
829
- } catch (e) {
830
- spinner.fail('Update failed - try manually: npm install -g hedgequantx@latest');
831
- console.log(chalk.gray(` Error: ${e.message}`));
832
- console.log();
833
- await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
834
- return;
835
- }
836
-
837
- spinner.succeed('CLI updated!');
838
- console.log();
839
- console.log(chalk.green(` ✓ Updated: v${currentVersion} -> v${latestVersion}`));
840
- console.log();
841
- console.log(chalk.cyan(' Restarting HedgeQuantX CLI...'));
842
- console.log();
843
-
844
- // Small delay so user can see the message
845
- await new Promise(resolve => setTimeout(resolve, 1500));
846
-
847
- // Restart the CLI automatically
848
- const { spawn } = require('child_process');
849
- const child = spawn('hedgequantx', [], {
850
- stdio: 'inherit',
851
- detached: true,
852
- shell: true
853
- });
854
- child.unref();
855
- process.exit(0);
856
-
857
- } catch (error) {
858
- spinner.fail('Update failed: ' + error.message);
859
- console.log(chalk.yellow(' Try manually: npm install -g hedgequantx@latest'));
860
- console.log();
861
- await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
862
- }
863
- };
864
-
865
278
  /**
866
279
  * Main application loop
867
280
  */
@@ -965,4 +378,4 @@ const run = async () => {
965
378
  }
966
379
  };
967
380
 
968
- module.exports = { run, banner, loginPrompt, mainMenu, dashboardMenu };
381
+ module.exports = { run, banner, mainMenu, dashboardMenu };
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Connection Menus - PropFirm platform selection and login
3
+ * Handles ProjectX, Rithmic, and Tradovate connections
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const inquirer = require('inquirer');
8
+ const ora = require('ora');
9
+
10
+ const { ProjectXService, connections } = require('../services');
11
+ const { RithmicService } = require('../services/rithmic');
12
+ const { TradovateService } = require('../services/tradovate');
13
+ const { getPropFirmsByPlatform } = require('../config');
14
+ const { getDevice, getLogoWidth, centerText } = require('../ui');
15
+ const { validateUsername, validatePassword } = require('../security');
16
+
17
+ /**
18
+ * Login prompt with validation
19
+ * @param {string} propfirmName - PropFirm display name
20
+ * @returns {Promise<{username: string, password: string}>}
21
+ */
22
+ const loginPrompt = async (propfirmName) => {
23
+ const device = getDevice();
24
+ console.log();
25
+ console.log(chalk.cyan(`Connecting to ${propfirmName}...`));
26
+ console.log();
27
+
28
+ const credentials = await inquirer.prompt([
29
+ {
30
+ type: 'input',
31
+ name: 'username',
32
+ message: chalk.white.bold('Username:'),
33
+ validate: (input) => {
34
+ try {
35
+ validateUsername(input);
36
+ return true;
37
+ } catch (e) {
38
+ return e.message;
39
+ }
40
+ }
41
+ },
42
+ {
43
+ type: 'password',
44
+ name: 'password',
45
+ message: chalk.white.bold('Password:'),
46
+ mask: '*',
47
+ validate: (input) => {
48
+ try {
49
+ validatePassword(input);
50
+ return true;
51
+ } catch (e) {
52
+ return e.message;
53
+ }
54
+ }
55
+ }
56
+ ]);
57
+
58
+ return credentials;
59
+ };
60
+
61
+ /**
62
+ * ProjectX platform connection menu
63
+ */
64
+ const projectXMenu = async () => {
65
+ const propfirms = getPropFirmsByPlatform('ProjectX');
66
+ const boxWidth = getLogoWidth();
67
+ const innerWidth = boxWidth - 2;
68
+ const numCols = 3;
69
+ const colWidth = Math.floor(innerWidth / numCols);
70
+
71
+ // Build numbered list
72
+ const numbered = propfirms.map((pf, i) => ({
73
+ num: i + 1,
74
+ key: pf.key,
75
+ name: pf.displayName
76
+ }));
77
+
78
+ // PropFirm selection box
79
+ console.log();
80
+ console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
81
+ console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM', innerWidth)) + chalk.cyan('║'));
82
+ console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
83
+
84
+ // Display in 3 columns with fixed width alignment
85
+ const rows = Math.ceil(numbered.length / numCols);
86
+ const maxNum = numbered.length;
87
+ const numWidth = maxNum >= 10 ? 4 : 3; // [XX] or [X]
88
+
89
+ for (let row = 0; row < rows; row++) {
90
+ let line = '';
91
+ for (let col = 0; col < numCols; col++) {
92
+ const idx = row + col * rows;
93
+ if (idx < numbered.length) {
94
+ const item = numbered[idx];
95
+ const numStr = item.num.toString().padStart(2, ' ');
96
+ const coloredText = chalk.cyan(`[${numStr}]`) + ' ' + chalk.white(item.name);
97
+ const textLen = 4 + 1 + item.name.length; // [XX] + space + name
98
+ const padding = colWidth - textLen - 2;
99
+ line += ' ' + coloredText + ' '.repeat(Math.max(0, padding));
100
+ } else {
101
+ line += ' '.repeat(colWidth);
102
+ }
103
+ }
104
+ // Adjust for exact width
105
+ const lineLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
106
+ const adjust = innerWidth - lineLen;
107
+ console.log(chalk.cyan('║') + line + ' '.repeat(Math.max(0, adjust)) + chalk.cyan('║'));
108
+ }
109
+
110
+ console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
111
+ const backText = ' ' + chalk.red('[X] Back');
112
+ const backLen = '[X] Back'.length + 2;
113
+ console.log(chalk.cyan('║') + backText + ' '.repeat(innerWidth - backLen) + chalk.cyan('║'));
114
+ console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
115
+ console.log();
116
+
117
+ const validInputs = numbered.map(n => n.num.toString());
118
+ validInputs.push('x', 'X');
119
+
120
+ const { action } = await inquirer.prompt([
121
+ {
122
+ type: 'input',
123
+ name: 'action',
124
+ message: chalk.cyan(`Enter choice (1-${numbered.length}/X):`),
125
+ validate: (input) => {
126
+ if (validInputs.includes(input)) return true;
127
+ return `Please enter 1-${numbered.length} or X`;
128
+ }
129
+ }
130
+ ]);
131
+
132
+ if (action.toLowerCase() === 'x') return null;
133
+
134
+ const selectedIdx = parseInt(action) - 1;
135
+ const selectedPropfirm = numbered[selectedIdx];
136
+
137
+ const credentials = await loginPrompt(selectedPropfirm.name);
138
+ const spinner = ora('Authenticating...').start();
139
+
140
+ try {
141
+ const service = new ProjectXService(selectedPropfirm.key);
142
+ const result = await service.login(credentials.username, credentials.password);
143
+
144
+ if (result.success) {
145
+ await service.getUser();
146
+ connections.add('projectx', service, service.propfirm.name);
147
+ spinner.succeed(`Connected to ${service.propfirm.name}`);
148
+ return service;
149
+ } else {
150
+ spinner.fail(result.error || 'Authentication failed');
151
+ return null;
152
+ }
153
+ } catch (error) {
154
+ spinner.fail(error.message);
155
+ return null;
156
+ }
157
+ };
158
+
159
+ /**
160
+ * Rithmic platform connection menu
161
+ */
162
+ const rithmicMenu = async () => {
163
+ const propfirms = getPropFirmsByPlatform('Rithmic');
164
+ const boxWidth = getLogoWidth();
165
+ const innerWidth = boxWidth - 2;
166
+ const numCols = 3;
167
+ const colWidth = Math.floor(innerWidth / numCols);
168
+
169
+ // Build numbered list
170
+ const numbered = propfirms.map((pf, i) => ({
171
+ num: i + 1,
172
+ key: pf.key,
173
+ name: pf.displayName,
174
+ systemName: pf.rithmicSystem
175
+ }));
176
+
177
+ // PropFirm selection box
178
+ console.log();
179
+ console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
180
+ console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM (RITHMIC)', innerWidth)) + chalk.cyan('║'));
181
+ console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
182
+
183
+ // Display in 3 columns with fixed width alignment
184
+ const rows = Math.ceil(numbered.length / numCols);
185
+
186
+ for (let row = 0; row < rows; row++) {
187
+ let line = '';
188
+ for (let col = 0; col < numCols; col++) {
189
+ const idx = row + col * rows;
190
+ if (idx < numbered.length) {
191
+ const item = numbered[idx];
192
+ const numStr = item.num.toString().padStart(2, ' ');
193
+ const coloredText = chalk.cyan(`[${numStr}]`) + ' ' + chalk.white(item.name);
194
+ const textLen = 4 + 1 + item.name.length;
195
+ const padding = colWidth - textLen - 2;
196
+ line += ' ' + coloredText + ' '.repeat(Math.max(0, padding));
197
+ } else {
198
+ line += ' '.repeat(colWidth);
199
+ }
200
+ }
201
+ const lineLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
202
+ const adjust = innerWidth - lineLen;
203
+ console.log(chalk.cyan('║') + line + ' '.repeat(Math.max(0, adjust)) + chalk.cyan('║'));
204
+ }
205
+
206
+ console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
207
+ const backText = ' ' + chalk.red('[X] Back');
208
+ const backLen = '[X] Back'.length + 2;
209
+ console.log(chalk.cyan('║') + backText + ' '.repeat(innerWidth - backLen) + chalk.cyan('║'));
210
+ console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
211
+ console.log();
212
+
213
+ const validInputs = numbered.map(n => n.num.toString());
214
+ validInputs.push('x', 'X');
215
+
216
+ const { action } = await inquirer.prompt([
217
+ {
218
+ type: 'input',
219
+ name: 'action',
220
+ message: chalk.cyan(`Enter choice (1-${numbered.length}/X):`),
221
+ validate: (input) => {
222
+ if (validInputs.includes(input)) return true;
223
+ return `Please enter 1-${numbered.length} or X`;
224
+ }
225
+ }
226
+ ]);
227
+
228
+ if (action.toLowerCase() === 'x') return null;
229
+
230
+ const selectedIdx = parseInt(action) - 1;
231
+ const selectedPropfirm = numbered[selectedIdx];
232
+
233
+ const credentials = await loginPrompt(selectedPropfirm.name);
234
+ const spinner = ora('Connecting to Rithmic...').start();
235
+
236
+ try {
237
+ const service = new RithmicService(selectedPropfirm.key);
238
+ const result = await service.login(credentials.username, credentials.password);
239
+
240
+ if (result.success) {
241
+ spinner.text = 'Fetching accounts...';
242
+ const accResult = await service.getTradingAccounts();
243
+
244
+ connections.add('rithmic', service, service.propfirm.name);
245
+ spinner.succeed(`Connected to ${service.propfirm.name} (${accResult.accounts?.length || 0} accounts)`);
246
+
247
+ // Small pause to see the success message
248
+ await new Promise(r => setTimeout(r, 1500));
249
+ return service;
250
+ } else {
251
+ spinner.fail(result.error || 'Authentication failed');
252
+ await new Promise(r => setTimeout(r, 2000));
253
+ return null;
254
+ }
255
+ } catch (error) {
256
+ spinner.fail(`Connection error: ${error.message}`);
257
+ await new Promise(r => setTimeout(r, 2000));
258
+ return null;
259
+ }
260
+ };
261
+
262
+ /**
263
+ * Tradovate platform connection menu
264
+ */
265
+ const tradovateMenu = async () => {
266
+ const propfirms = getPropFirmsByPlatform('Tradovate');
267
+ const boxWidth = getLogoWidth();
268
+ const innerWidth = boxWidth - 2;
269
+
270
+ // Build numbered list
271
+ const numbered = propfirms.map((pf, i) => ({
272
+ num: i + 1,
273
+ key: pf.key,
274
+ name: pf.displayName
275
+ }));
276
+
277
+ // PropFirm selection box
278
+ console.log();
279
+ console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
280
+ console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM (TRADOVATE)', innerWidth)) + chalk.cyan('║'));
281
+ console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
282
+
283
+ // Display propfirms
284
+ for (const item of numbered) {
285
+ const numStr = item.num.toString().padStart(2, ' ');
286
+ const text = ' ' + chalk.cyan(`[${numStr}]`) + ' ' + chalk.white(item.name);
287
+ const textLen = 4 + 1 + item.name.length + 2;
288
+ console.log(chalk.cyan('║') + text + ' '.repeat(innerWidth - textLen) + chalk.cyan('║'));
289
+ }
290
+
291
+ console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
292
+ const backText = ' ' + chalk.red('[X] Back');
293
+ const backLen = '[X] Back'.length + 2;
294
+ console.log(chalk.cyan('║') + backText + ' '.repeat(innerWidth - backLen) + chalk.cyan('║'));
295
+ console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
296
+ console.log();
297
+
298
+ const validInputs = numbered.map(n => n.num.toString());
299
+ validInputs.push('x', 'X');
300
+
301
+ const { action } = await inquirer.prompt([
302
+ {
303
+ type: 'input',
304
+ name: 'action',
305
+ message: chalk.cyan(`Enter choice (1-${numbered.length}/X):`),
306
+ validate: (input) => {
307
+ if (validInputs.includes(input)) return true;
308
+ return `Please enter 1-${numbered.length} or X`;
309
+ }
310
+ }
311
+ ]);
312
+
313
+ if (action.toLowerCase() === 'x') return null;
314
+
315
+ const selectedIdx = parseInt(action) - 1;
316
+ const selectedPropfirm = numbered[selectedIdx];
317
+
318
+ const credentials = await loginPrompt(selectedPropfirm.name);
319
+ const spinner = ora('Connecting to Tradovate...').start();
320
+
321
+ try {
322
+ const service = new TradovateService(selectedPropfirm.key);
323
+ const result = await service.login(credentials.username, credentials.password);
324
+
325
+ if (result.success) {
326
+ spinner.text = 'Fetching accounts...';
327
+ await service.getTradingAccounts();
328
+
329
+ connections.add('tradovate', service, service.propfirm.name);
330
+ spinner.succeed(`Connected to ${service.propfirm.name}`);
331
+ return service;
332
+ } else {
333
+ spinner.fail(result.error || 'Authentication failed');
334
+ return null;
335
+ }
336
+ } catch (error) {
337
+ spinner.fail(error.message);
338
+ return null;
339
+ }
340
+ };
341
+
342
+ /**
343
+ * Add Prop Account menu (select platform)
344
+ */
345
+ const addPropAccountMenu = async () => {
346
+ const boxWidth = getLogoWidth();
347
+ const innerWidth = boxWidth - 2;
348
+ const col1Width = Math.floor(innerWidth / 2);
349
+ const col2Width = innerWidth - col1Width;
350
+
351
+ console.log();
352
+ console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
353
+ console.log(chalk.cyan('║') + chalk.white.bold(centerText('ADD PROP ACCOUNT', innerWidth)) + chalk.cyan('║'));
354
+ console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
355
+
356
+ const menuRow = (left, right) => {
357
+ const leftText = ' ' + left;
358
+ const rightText = right ? ' ' + right : '';
359
+ const leftLen = leftText.replace(/\x1b\[[0-9;]*m/g, '').length;
360
+ const rightLen = rightText.replace(/\x1b\[[0-9;]*m/g, '').length;
361
+ const leftPad = col1Width - leftLen;
362
+ const rightPad = col2Width - rightLen;
363
+ console.log(chalk.cyan('║') + leftText + ' '.repeat(Math.max(0, leftPad)) + rightText + ' '.repeat(Math.max(0, rightPad)) + chalk.cyan('║'));
364
+ };
365
+
366
+ menuRow(chalk.cyan('[1] ProjectX'), chalk.cyan('[2] Rithmic'));
367
+ menuRow(chalk.cyan('[3] Tradovate'), chalk.red('[X] Back'));
368
+
369
+ console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
370
+ console.log();
371
+
372
+ const { action } = await inquirer.prompt([
373
+ {
374
+ type: 'input',
375
+ name: 'action',
376
+ message: chalk.cyan('Enter choice (1/2/3/X):'),
377
+ validate: (input) => {
378
+ const valid = ['1', '2', '3', 'x', 'X'];
379
+ if (valid.includes(input)) return true;
380
+ return 'Please enter 1, 2, 3 or X';
381
+ }
382
+ }
383
+ ]);
384
+
385
+ const actionMap = {
386
+ '1': 'projectx',
387
+ '2': 'rithmic',
388
+ '3': 'tradovate',
389
+ 'x': null,
390
+ 'X': null
391
+ };
392
+
393
+ return actionMap[action];
394
+ };
395
+
396
+ module.exports = {
397
+ loginPrompt,
398
+ projectXMenu,
399
+ rithmicMenu,
400
+ tradovateMenu,
401
+ addPropAccountMenu
402
+ };
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Dashboard Menu - Main menu after login
3
+ * Shows connected PropFirms and navigation options
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const inquirer = require('inquirer');
8
+ const ora = require('ora');
9
+ const { execSync, spawn } = require('child_process');
10
+
11
+ const { connections } = require('../services');
12
+ const { getLogoWidth, centerText } = require('../ui');
13
+
14
+ /**
15
+ * Dashboard menu after login
16
+ * @param {Object} service - Connected service
17
+ */
18
+ const dashboardMenu = async (service) => {
19
+ const user = service.user;
20
+ const boxWidth = getLogoWidth();
21
+ const W = boxWidth - 2; // Same width as logo (inner width)
22
+
23
+ // Helper to center text
24
+ const centerLine = (text, width) => {
25
+ const pad = Math.floor((width - text.length) / 2);
26
+ return ' '.repeat(Math.max(0, pad)) + text + ' '.repeat(Math.max(0, width - pad - text.length));
27
+ };
28
+
29
+ // Helper to pad text left
30
+ const padLine = (text, width) => {
31
+ return ' ' + text + ' '.repeat(Math.max(0, width - text.length - 1));
32
+ };
33
+
34
+ // Dashboard box header
35
+ console.log();
36
+ console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
37
+ console.log(chalk.cyan('║') + chalk.yellow.bold(centerLine('Welcome, HQX Trader!', W)) + chalk.cyan('║'));
38
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
39
+
40
+ // Connection info - show all active connections in boxes (max 3 per row)
41
+ const allConns = connections.getAll();
42
+ if (allConns.length > 0) {
43
+ const maxPerRow = 3;
44
+ const boxPadding = 2; // padding inside each mini-box
45
+ const gap = 2; // gap between boxes
46
+
47
+ // Calculate box width based on number of connections (max 3)
48
+ const numBoxes = Math.min(allConns.length, maxPerRow);
49
+ const totalGaps = (numBoxes - 1) * gap;
50
+ const connBoxWidth = Math.floor((W - totalGaps - 2) / numBoxes); // -2 for outer padding
51
+
52
+ // Process connections in rows of 3
53
+ for (let rowStart = 0; rowStart < allConns.length; rowStart += maxPerRow) {
54
+ const rowConns = allConns.slice(rowStart, rowStart + maxPerRow);
55
+ const numInRow = rowConns.length;
56
+ const rowBoxWidth = Math.floor((W - (numInRow - 1) * gap - 2) / numInRow);
57
+
58
+ // Top border of boxes
59
+ let topLine = ' ';
60
+ for (let i = 0; i < numInRow; i++) {
61
+ topLine += '┌' + '─'.repeat(rowBoxWidth - 2) + '┐';
62
+ if (i < numInRow - 1) topLine += ' '.repeat(gap);
63
+ }
64
+ const topPad = W - topLine.length;
65
+ console.log(chalk.cyan('║') + chalk.green(topLine) + ' '.repeat(Math.max(0, topPad)) + chalk.cyan('║'));
66
+
67
+ // Content of boxes
68
+ let contentLine = ' ';
69
+ for (let i = 0; i < numInRow; i++) {
70
+ const connText = rowConns[i].propfirm || rowConns[i].type || 'Connected';
71
+ const truncated = connText.length > rowBoxWidth - 4 ? connText.slice(0, rowBoxWidth - 7) + '...' : connText;
72
+ const innerWidth = rowBoxWidth - 4; // -2 for borders, -2 for padding
73
+ const textPad = Math.floor((innerWidth - truncated.length) / 2);
74
+ const textPadRight = innerWidth - truncated.length - textPad;
75
+ contentLine += '│ ' + ' '.repeat(textPad) + truncated + ' '.repeat(textPadRight) + ' │';
76
+ if (i < numInRow - 1) contentLine += ' '.repeat(gap);
77
+ }
78
+ const contentPad = W - contentLine.length;
79
+ console.log(chalk.cyan('║') + chalk.green(contentLine) + ' '.repeat(Math.max(0, contentPad)) + chalk.cyan('║'));
80
+
81
+ // Bottom border of boxes
82
+ let bottomLine = ' ';
83
+ for (let i = 0; i < numInRow; i++) {
84
+ bottomLine += '└' + '─'.repeat(rowBoxWidth - 2) + '┘';
85
+ if (i < numInRow - 1) bottomLine += ' '.repeat(gap);
86
+ }
87
+ const bottomPad = W - bottomLine.length;
88
+ console.log(chalk.cyan('║') + chalk.green(bottomLine) + ' '.repeat(Math.max(0, bottomPad)) + chalk.cyan('║'));
89
+ }
90
+ }
91
+
92
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
93
+
94
+ // Menu options in 2 columns
95
+ const col1Width = Math.floor(W / 2);
96
+ const col2Width = W - col1Width;
97
+
98
+ const menuRow = (left, right) => {
99
+ const leftPlain = left.replace(/\x1b\[[0-9;]*m/g, '');
100
+ const rightPlain = right ? right.replace(/\x1b\[[0-9;]*m/g, '') : '';
101
+ const leftPad = ' '.repeat(Math.max(0, col1Width - leftPlain.length - 2));
102
+ const rightPad = ' '.repeat(Math.max(0, col2Width - rightPlain.length - 2));
103
+ console.log(chalk.cyan('║') + ' ' + left + leftPad + ' ' + (right || '') + rightPad + chalk.cyan('║'));
104
+ };
105
+
106
+ menuRow(chalk.cyan('[1] View Accounts'), chalk.cyan('[2] View Stats'));
107
+ menuRow(chalk.cyan('[+] Add Prop-Account'), chalk.cyan('[A] Algo-Trading'));
108
+ menuRow(chalk.yellow('[U] Update HQX'), chalk.red('[X] Disconnect'));
109
+
110
+ console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
111
+ console.log();
112
+
113
+ const { action } = await inquirer.prompt([
114
+ {
115
+ type: 'input',
116
+ name: 'action',
117
+ message: chalk.cyan('Enter choice (1/2/+/A/U/X):'),
118
+ validate: (input) => {
119
+ const valid = ['1', '2', '+', 'a', 'A', 'u', 'U', 'x', 'X'];
120
+ if (valid.includes(input)) return true;
121
+ return 'Please enter a valid option';
122
+ }
123
+ }
124
+ ]);
125
+
126
+ // Map input to action
127
+ const actionMap = {
128
+ '1': 'accounts',
129
+ '2': 'stats',
130
+ '+': 'add_prop_account',
131
+ 'a': 'algotrading',
132
+ 'A': 'algotrading',
133
+ 'u': 'update',
134
+ 'U': 'update',
135
+ 'x': 'disconnect',
136
+ 'X': 'disconnect'
137
+ };
138
+
139
+ return actionMap[action] || 'accounts';
140
+ };
141
+
142
+ /**
143
+ * Handles the update process with auto-restart
144
+ */
145
+ const handleUpdate = async () => {
146
+ const pkg = require('../../package.json');
147
+ const currentVersion = pkg.version;
148
+ const spinner = ora('Checking for updates...').start();
149
+
150
+ try {
151
+ // Check latest version on npm
152
+ spinner.text = 'Checking npm registry...';
153
+ let latestVersion;
154
+ try {
155
+ latestVersion = execSync('npm view hedgequantx version', { stdio: 'pipe' }).toString().trim();
156
+ } catch (e) {
157
+ spinner.fail('Cannot reach npm registry');
158
+ console.log();
159
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
160
+ return;
161
+ }
162
+
163
+ if (currentVersion === latestVersion) {
164
+ spinner.succeed('Already up to date!');
165
+ console.log();
166
+ console.log(chalk.green(` ✓ You have the latest version of HedgeQuantX CLI: v${currentVersion}`));
167
+ console.log();
168
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
169
+ return;
170
+ }
171
+
172
+ // Update via npm
173
+ spinner.text = `Updating v${currentVersion} -> v${latestVersion}...`;
174
+ try {
175
+ execSync('npm install -g hedgequantx@latest', { stdio: 'pipe' });
176
+ } catch (e) {
177
+ spinner.fail('Update failed - try manually: npm install -g hedgequantx@latest');
178
+ console.log(chalk.gray(` Error: ${e.message}`));
179
+ console.log();
180
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
181
+ return;
182
+ }
183
+
184
+ spinner.succeed('CLI updated!');
185
+ console.log();
186
+ console.log(chalk.green(` ✓ Updated: v${currentVersion} -> v${latestVersion}`));
187
+ console.log();
188
+ console.log(chalk.cyan(' Restarting HedgeQuantX CLI...'));
189
+ console.log();
190
+
191
+ // Small delay so user can see the message
192
+ await new Promise(resolve => setTimeout(resolve, 1500));
193
+
194
+ // Restart the CLI automatically
195
+ const child = spawn('hedgequantx', [], {
196
+ stdio: 'inherit',
197
+ detached: true,
198
+ shell: true
199
+ });
200
+ child.unref();
201
+ process.exit(0);
202
+
203
+ } catch (error) {
204
+ spinner.fail('Update failed: ' + error.message);
205
+ console.log(chalk.yellow(' Try manually: npm install -g hedgequantx@latest'));
206
+ console.log();
207
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
208
+ }
209
+ };
210
+
211
+ module.exports = {
212
+ dashboardMenu,
213
+ handleUpdate
214
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Menus - Export all menu modules
3
+ */
4
+
5
+ module.exports = {
6
+ ...require('./connect'),
7
+ ...require('./dashboard')
8
+ };
@@ -193,8 +193,9 @@ class AlgoUI {
193
193
  this._line(chalk.cyan(BOX.V) + chalk.white(left) + ' '.repeat(midPad) + chalk.cyan(mid) + ' '.repeat(space - midPad - mid.length) + chalk.yellow(right) + chalk.cyan(BOX.V));
194
194
  this._line(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
195
195
 
196
- // Logs (newest first)
197
- const visible = [...logs].reverse().slice(0, maxLogs);
196
+ // Logs (oldest at top, newest at bottom - like a terminal)
197
+ // Take the last maxLogs entries (most recent), keep chronological order
198
+ const visible = logs.slice(-maxLogs);
198
199
 
199
200
  if (visible.length === 0) {
200
201
  this._line(chalk.cyan(BOX.V) + chalk.gray(fitToWidth(' Waiting for activity...', W)) + chalk.cyan(BOX.V));
@@ -202,15 +203,17 @@ class AlgoUI {
202
203
  this._line(chalk.cyan(BOX.V) + ' '.repeat(W) + chalk.cyan(BOX.V));
203
204
  }
204
205
  } else {
206
+ // First draw empty lines for padding (so logs stick to bottom)
207
+ for (let i = visible.length; i < maxLogs; i++) {
208
+ this._line(chalk.cyan(BOX.V) + ' '.repeat(W) + chalk.cyan(BOX.V));
209
+ }
210
+ // Then draw logs (oldest first, newest last/at bottom)
205
211
  visible.forEach(log => {
206
212
  const color = LOG_COLORS[log.type] || chalk.white;
207
213
  const icon = LOG_ICONS[log.type] || LOG_ICONS.info;
208
214
  const line = ` [${log.timestamp}] ${icon} ${log.message}`;
209
215
  this._line(chalk.cyan(BOX.V) + color(fitToWidth(line, W)) + chalk.cyan(BOX.V));
210
216
  });
211
- for (let i = visible.length; i < maxLogs; i++) {
212
- this._line(chalk.cyan(BOX.V) + ' '.repeat(W) + chalk.cyan(BOX.V));
213
- }
214
217
  }
215
218
 
216
219
  // Bottom border