hedgequantx 1.2.122 → 1.2.125

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/pages/algo.js +118 -54
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "1.2.122",
3
+ "version": "1.2.125",
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/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();