hedgequantx 1.2.122 → 1.2.126

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.122",
3
+ "version": "1.2.126",
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
@@ -666,7 +666,7 @@ const dashboardMenu = async (service) => {
666
666
  * Handles the update process with auto-restart
667
667
  */
668
668
  const handleUpdate = async () => {
669
- const { spawn, execSync: exec } = require('child_process');
669
+ const { execSync: exec } = require('child_process');
670
670
  const pkg = require('../package.json');
671
671
  const currentVersion = pkg.version;
672
672
  const spinner = ora('Checking for updates...').start();
@@ -679,38 +679,50 @@ const handleUpdate = async () => {
679
679
  latestVersion = exec('npm view hedgequantx version', { stdio: 'pipe' }).toString().trim();
680
680
  } catch (e) {
681
681
  spinner.fail('Cannot reach npm registry');
682
+ console.log();
683
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
682
684
  return;
683
685
  }
684
686
 
685
687
  if (currentVersion === latestVersion) {
686
688
  spinner.succeed('Already up to date!');
687
- console.log(chalk.cyan(` Version: v${currentVersion}`));
689
+ console.log();
690
+ console.log(chalk.green(` ✓ You have the latest version of HedgeQuantX CLI: v${currentVersion}`));
691
+ console.log();
692
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
688
693
  return;
689
694
  }
690
695
 
691
696
  // Update via npm
692
697
  spinner.text = `Updating v${currentVersion} -> v${latestVersion}...`;
693
698
  try {
694
- execSync('npm install -g hedgequantx@latest', { stdio: 'pipe' });
699
+ exec('npm install -g hedgequantx@latest', { stdio: 'pipe' });
695
700
  } catch (e) {
696
701
  spinner.fail('Update failed - try manually: npm install -g hedgequantx@latest');
697
702
  console.log(chalk.gray(` Error: ${e.message}`));
703
+ console.log();
704
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
698
705
  return;
699
706
  }
700
707
 
701
708
  spinner.succeed('CLI updated!');
702
709
  console.log();
703
- console.log(chalk.green(` Updated: v${currentVersion} -> v${latestVersion}`));
710
+ console.log(chalk.green(` Updated: v${currentVersion} -> v${latestVersion}`));
704
711
  console.log();
705
- console.log(chalk.cyan(' Please restart HQX to apply changes.'));
712
+ console.log(chalk.cyan(' Restarting HQX...'));
706
713
  console.log();
707
714
 
715
+ // Small delay so user can see the message
716
+ await new Promise(resolve => setTimeout(resolve, 1500));
717
+
708
718
  // Exit so user can restart
709
719
  process.exit(0);
710
720
 
711
721
  } catch (error) {
712
722
  spinner.fail('Update failed: ' + error.message);
713
723
  console.log(chalk.yellow(' Try manually: npm install -g hedgequantx@latest'));
724
+ console.log();
725
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
714
726
  }
715
727
  };
716
728
 
package/src/pages/algo.js CHANGED
@@ -473,6 +473,7 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
473
473
  success: chalk.green,
474
474
  signal: chalk.yellow.bold,
475
475
  trade: chalk.green.bold,
476
+ loss: chalk.magenta.bold,
476
477
  error: chalk.red,
477
478
  warning: chalk.yellow
478
479
  };
@@ -499,10 +500,12 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
499
500
  if (logs.length > MAX_LOGS) logs.shift(); // Remove oldest from top
500
501
  };
501
502
 
502
- // Print log and refresh display
503
+ // Print log - just add to buffer, spinner interval will refresh display
504
+ // This prevents display flicker from multiple concurrent displayUI() calls
503
505
  const printLog = (type, message) => {
504
506
  addLog(type, message);
505
- displayUI();
507
+ // Don't call displayUI() here - let the spinner interval handle it
508
+ // This prevents flickering when logs arrive rapidly
506
509
  };
507
510
 
508
511
  // Check market hours
@@ -527,19 +530,38 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
527
530
  };
528
531
 
529
532
  // Display full UI with logs (newest first at top)
530
- // Use ANSI escape codes to avoid flickering
531
533
  let firstDraw = true;
534
+ let isDrawing = false; // Mutex to prevent concurrent draws
535
+
536
+ // Build entire screen as a single string buffer to write atomically
537
+ let screenBuffer = '';
538
+
539
+ const bufferLine = (text) => {
540
+ screenBuffer += text + '\x1B[K\n'; // Add text + clear to EOL + newline
541
+ };
542
+
543
+ // Legacy function for compatibility
544
+ const printLine = bufferLine;
545
+
532
546
  const displayUI = () => {
533
- // First time: clear screen and hide cursor, after: just move cursor to top
547
+ // Prevent concurrent draws
548
+ if (isDrawing) return;
549
+ isDrawing = true;
550
+
551
+ // Reset buffer
552
+ screenBuffer = '';
553
+
534
554
  if (firstDraw) {
535
- console.clear();
536
- process.stdout.write('\x1B[?25l'); // Hide cursor
555
+ // Switch to alternate screen buffer - isolates our display
556
+ screenBuffer += '\x1B[?1049h'; // Enter alternate screen
557
+ screenBuffer += '\x1B[?25l'; // Hide cursor
558
+ screenBuffer += '\x1B[2J'; // Clear screen
537
559
  firstDraw = false;
538
- } else {
539
- // Move cursor to top-left without clearing (avoids flicker)
540
- process.stdout.write('\x1B[H');
541
560
  }
542
561
 
562
+ // Move cursor to home position
563
+ screenBuffer += '\x1B[H';
564
+
543
565
  // Stats
544
566
  const pnlColor = stats.pnl >= 0 ? chalk.green : chalk.red;
545
567
  const pnlStr = (stats.pnl >= 0 ? '+$' : '-$') + Math.abs(stats.pnl).toFixed(2);
@@ -576,25 +598,25 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
576
598
  return text + ' '.repeat(width - text.length);
577
599
  };
578
600
 
579
- console.log();
580
- console.log(chalk.cyan(TOP));
601
+ printLine('');
602
+ printLine(chalk.cyan(TOP));
581
603
  // Logo = 87 chars cyan + 9 chars yellow = 96 total
582
- console.log(chalk.cyan(V) + chalk.cyan(' ██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗') + chalk.yellow('██╗ ██╗') + ' ' + chalk.cyan(V));
583
- console.log(chalk.cyan(V) + chalk.cyan(' ██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝') + chalk.yellow('╚██╗██╔╝') + ' ' + chalk.cyan(V));
584
- console.log(chalk.cyan(V) + chalk.cyan(' ███████║█████╗ ██║ ██║██║ ███╗█████╗ ██║ ██║██║ ██║███████║██╔██╗ ██║ ██║ ') + chalk.yellow(' ╚███╔╝ ') + ' ' + chalk.cyan(V));
585
- console.log(chalk.cyan(V) + chalk.cyan(' ██╔══██║██╔══╝ ██║ ██║██║ ██║██╔══╝ ██║▄▄ ██║██║ ██║██╔══██║██║╚██╗██║ ██║ ') + chalk.yellow(' ██╔██╗ ') + ' ' + chalk.cyan(V));
586
- console.log(chalk.cyan(V) + chalk.cyan(' ██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ') + chalk.yellow('██╔╝ ██╗') + ' ' + chalk.cyan(V));
587
- console.log(chalk.cyan(V) + chalk.cyan(' ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ') + chalk.yellow('╚═╝ ╚═╝') + ' ' + chalk.cyan(V));
588
- console.log(chalk.cyan(MID));
604
+ printLine(chalk.cyan(V) + chalk.cyan(' ██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗') + chalk.yellow('██╗ ██╗') + ' ' + chalk.cyan(V));
605
+ printLine(chalk.cyan(V) + chalk.cyan(' ██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝') + chalk.yellow('╚██╗██╔╝') + ' ' + chalk.cyan(V));
606
+ printLine(chalk.cyan(V) + chalk.cyan(' ███████║█████╗ ██║ ██║██║ ███╗█████╗ ██║ ██║██║ ██║███████║██╔██╗ ██║ ██║ ') + chalk.yellow(' ╚███╔╝ ') + ' ' + chalk.cyan(V));
607
+ printLine(chalk.cyan(V) + chalk.cyan(' ██╔══██║██╔══╝ ██║ ██║██║ ██║██╔══╝ ██║▄▄ ██║██║ ██║██╔══██║██║╚██╗██║ ██║ ') + chalk.yellow(' ██╔██╗ ') + ' ' + chalk.cyan(V));
608
+ printLine(chalk.cyan(V) + chalk.cyan(' ██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ') + chalk.yellow('██╔╝ ██╗') + ' ' + chalk.cyan(V));
609
+ printLine(chalk.cyan(V) + chalk.cyan(' ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ') + chalk.yellow('╚═╝ ╚═╝') + ' ' + chalk.cyan(V));
610
+ printLine(chalk.cyan(MID));
589
611
 
590
612
  // Centered title
591
613
  const title1 = `Prop Futures Algo Trading v${version}`;
592
- console.log(chalk.cyan(V) + chalk.white(center(title1, W)) + chalk.cyan(V));
593
- console.log(chalk.cyan(MID));
614
+ printLine(chalk.cyan(V) + chalk.white(center(title1, W)) + chalk.cyan(V));
615
+ printLine(chalk.cyan(MID));
594
616
 
595
617
  // Centered subtitle
596
618
  const title2 = 'HQX Ultra-Scalping Algorithm';
597
- console.log(chalk.cyan(V) + chalk.yellow(center(title2, W)) + chalk.cyan(V));
619
+ printLine(chalk.cyan(V) + chalk.yellow(center(title2, W)) + chalk.cyan(V));
598
620
 
599
621
  // Grid layout for metrics - 2 columns per row, 4 rows
600
622
  // Row 1: Account | Symbol + Qty
@@ -644,15 +666,15 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
644
666
  const GRID_BOT = '\u2560' + '\u2550'.repeat(colL) + '\u2567' + '\u2550'.repeat(colR) + '\u2563';
645
667
 
646
668
  // Print grid
647
- console.log(chalk.cyan(GRID_TOP));
648
- console.log(chalk.cyan(V) + r1c1.padded + chalk.cyan(VS) + r1c2 + chalk.cyan(V));
649
- console.log(chalk.cyan(GRID_MID));
650
- console.log(chalk.cyan(V) + r2c1.padded + chalk.cyan(VS) + r2c2.padded + chalk.cyan(V));
651
- console.log(chalk.cyan(GRID_MID));
652
- console.log(chalk.cyan(V) + r3c1.padded + chalk.cyan(VS) + r3c2.padded + chalk.cyan(V));
653
- console.log(chalk.cyan(GRID_MID));
654
- console.log(chalk.cyan(V) + r4c1 + chalk.cyan(VS) + r4c2.padded + chalk.cyan(V));
655
- console.log(chalk.cyan(GRID_BOT));
669
+ printLine(chalk.cyan(GRID_TOP));
670
+ printLine(chalk.cyan(V) + r1c1.padded + chalk.cyan(VS) + r1c2 + chalk.cyan(V));
671
+ printLine(chalk.cyan(GRID_MID));
672
+ printLine(chalk.cyan(V) + r2c1.padded + chalk.cyan(VS) + r2c2.padded + chalk.cyan(V));
673
+ printLine(chalk.cyan(GRID_MID));
674
+ printLine(chalk.cyan(V) + r3c1.padded + chalk.cyan(VS) + r3c2.padded + chalk.cyan(V));
675
+ printLine(chalk.cyan(GRID_MID));
676
+ printLine(chalk.cyan(V) + r4c1 + chalk.cyan(VS) + r4c2.padded + chalk.cyan(V));
677
+ printLine(chalk.cyan(GRID_BOT));
656
678
 
657
679
  // Activity log header with spinner and centered date
658
680
  spinnerFrame = (spinnerFrame + 1) % spinnerChars.length;
@@ -667,19 +689,42 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
667
689
  const datePad = Math.max(0, Math.floor((midSpace - dateCentered.length) / 2));
668
690
  const remainingPad = Math.max(0, midSpace - datePad - dateCentered.length);
669
691
  const dateSection = ' '.repeat(datePad) + chalk.cyan(dateCentered) + ' '.repeat(remainingPad);
670
- console.log(chalk.cyan(V) + chalk.white(actLeft) + dateSection + chalk.yellow(actRight) + chalk.cyan(V));
671
- console.log(chalk.cyan(BOT));
692
+ bufferLine(chalk.cyan(V) + chalk.white(actLeft) + dateSection + chalk.yellow(actRight) + chalk.cyan(V));
693
+ bufferLine(chalk.cyan(MID));
694
+
695
+ // Helper to strip ANSI codes for length calculation
696
+ const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*m/g, '');
672
697
 
673
- // Logs (without borders) - newest first, show more logs
674
- // Align with left border of rectangle above ( = 1 char + 1 space)
698
+ // Helper to truncate and pad text to exact width W
699
+ const fitToWidth = (text, width) => {
700
+ const plainText = stripAnsi(text);
701
+ if (plainText.length > width) {
702
+ // Truncate - find where to cut in original string
703
+ let count = 0;
704
+ let cutIndex = 0;
705
+ for (let i = 0; i < text.length && count < width - 3; i++) {
706
+ if (text[i] === '\x1B') {
707
+ // Skip ANSI sequence
708
+ while (i < text.length && text[i] !== 'm') i++;
709
+ } else {
710
+ count++;
711
+ cutIndex = i + 1;
712
+ }
713
+ }
714
+ return text.substring(0, cutIndex) + '...';
715
+ }
716
+ return text + ' '.repeat(width - plainText.length);
717
+ };
718
+
719
+ // Logs inside the rectangle - newest first, max 50 lines
675
720
  const MAX_VISIBLE_LOGS = 50;
676
- console.log();
677
721
 
678
722
  if (logs.length === 0) {
679
- console.log(chalk.gray(' Waiting for activity...') + '\x1B[K');
680
- // Fill remaining lines with empty
723
+ const emptyLine = ' Waiting for activity...';
724
+ bufferLine(chalk.cyan(V) + chalk.gray(fitToWidth(emptyLine, W)) + chalk.cyan(V));
725
+ // Fill remaining lines
681
726
  for (let i = 0; i < MAX_VISIBLE_LOGS - 1; i++) {
682
- console.log('\x1B[K');
727
+ bufferLine(chalk.cyan(V) + ' '.repeat(W) + chalk.cyan(V));
683
728
  }
684
729
  } else {
685
730
  // Show newest first (reverse), limited to MAX_VISIBLE_LOGS
@@ -687,25 +732,33 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
687
732
  reversedLogs.forEach(log => {
688
733
  const color = typeColors[log.type] || chalk.white;
689
734
  const icon = getIcon(log.type);
690
- // Align with rectangle: 1 space to match content after ║
691
- const logLine = ` [${log.timestamp}] ${icon} ${log.message}`;
692
- console.log(color(logLine) + '\x1B[K');
735
+ // Build log line content (plain text, no color yet)
736
+ const logContent = ` [${log.timestamp}] ${icon} ${log.message}`;
737
+ // Fit to width then apply color
738
+ const fitted = fitToWidth(logContent, W);
739
+ bufferLine(chalk.cyan(V) + color(fitted) + chalk.cyan(V));
693
740
  });
694
741
  // Fill remaining lines with empty to keep fixed height
695
742
  for (let i = reversedLogs.length; i < MAX_VISIBLE_LOGS; i++) {
696
- console.log('\x1B[K');
743
+ bufferLine(chalk.cyan(V) + ' '.repeat(W) + chalk.cyan(V));
697
744
  }
698
745
  }
699
- // Clear anything below
700
- process.stdout.write('\x1B[J');
746
+
747
+ // Bottom border to close the rectangle
748
+ bufferLine(chalk.cyan(BOT));
749
+
750
+ // Write entire buffer atomically
751
+ process.stdout.write(screenBuffer);
752
+
753
+ isDrawing = false;
701
754
  };
702
755
 
703
- // Spinner interval to refresh UI
756
+ // Spinner interval to refresh UI - 250ms for stability
704
757
  const spinnerInterval = setInterval(() => {
705
- if (algoRunning) {
758
+ if (algoRunning && !isDrawing) {
706
759
  displayUI();
707
760
  }
708
- }, 100);
761
+ }, 250);
709
762
 
710
763
  // Connect to HQX Server
711
764
  const spinner = ora('Authenticating with HQX Server...').start();
@@ -745,7 +798,7 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
745
798
  // Setup event handlers - logs scroll down naturally
746
799
  hqxServer.on('latency', (data) => {
747
800
  latency = data.latency || 0;
748
- displayUI(); // Refresh UI with new latency
801
+ // Don't call displayUI() - spinner interval will refresh
749
802
  });
750
803
 
751
804
  hqxServer.on('log', (data) => {
@@ -776,12 +829,13 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
776
829
  printLog('trade', `Closed +$${data.pnl.toFixed(2)} (${data.reason || 'take_profit'})`);
777
830
  } else {
778
831
  stats.losses++;
779
- printLog('trade', `Closed -$${Math.abs(data.pnl).toFixed(2)} (${data.reason || 'stop_loss'})`);
832
+ printLog('loss', `Closed -$${Math.abs(data.pnl).toFixed(2)} (${data.reason || 'stop_loss'})`);
780
833
  }
781
834
  stats.winRate = stats.trades > 0 ? ((stats.wins / stats.trades) * 100).toFixed(1) : '0.0';
782
835
 
783
836
  // Print updated stats
784
- printLog('info', `Stats: Trades: ${stats.trades} | Wins: ${stats.wins} | P&L: $${stats.pnl.toFixed(2)}`);
837
+ const statsType = stats.pnl >= 0 ? 'info' : 'loss';
838
+ printLog(statsType, `Stats: Trades: ${stats.trades} | Wins: ${stats.wins} | P&L: $${stats.pnl.toFixed(2)}`);
785
839
 
786
840
  // Check daily target
787
841
  if (stats.pnl >= dailyTarget) {
@@ -805,8 +859,17 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
805
859
  });
806
860
 
807
861
  hqxServer.on('stats', (data) => {
808
- stats = { ...stats, ...data };
809
- // Don't print stats every time, only on trades
862
+ // Update stats from server
863
+ stats.trades = data.trades || stats.trades;
864
+ stats.wins = data.wins || stats.wins;
865
+ stats.losses = data.losses || stats.losses;
866
+ stats.signals = data.signals || stats.signals;
867
+ stats.winRate = data.winRate || stats.winRate;
868
+
869
+ // P&L = realized P&L + unrealized P&L from open position
870
+ const realizedPnl = data.pnl || 0;
871
+ const unrealizedPnl = data.position?.pnl || 0;
872
+ stats.pnl = realizedPnl + unrealizedPnl;
810
873
  });
811
874
 
812
875
  hqxServer.on('error', (data) => {
@@ -908,8 +971,9 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
908
971
  // Clear spinner interval
909
972
  clearInterval(spinnerInterval);
910
973
 
911
- // Show cursor again
912
- process.stdout.write('\x1B[?25h');
974
+ // Exit alternate screen buffer and show cursor
975
+ process.stdout.write('\x1B[?1049l'); // Exit alternate screen
976
+ process.stdout.write('\x1B[?25h'); // Show cursor
913
977
 
914
978
  // Stop algo
915
979
  console.log();