hedgequantx 1.2.121 → 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 +165 -55
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "1.2.121",
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
@@ -221,8 +221,54 @@ const selectSymbolMenu = async (service, account) => {
221
221
  });
222
222
  }
223
223
 
224
- // Sort by symbol code for better organization
224
+ // Sort by category: E-mini indices first, then Micro E-mini, then others
225
+ const getSymbolPriority = (contract) => {
226
+ const name = (contract.name || contract.symbol || '').toUpperCase();
227
+ const desc = (contract.description || '').toLowerCase();
228
+
229
+ // E-mini indices (NQ, ES, YM, RTY) - highest priority
230
+ if (name.match(/^(NQ|ES|YM|RTY)[A-Z]\d/) && !name.startsWith('M')) {
231
+ if (name.startsWith('NQ')) return 10;
232
+ if (name.startsWith('ES')) return 11;
233
+ if (name.startsWith('YM')) return 12;
234
+ if (name.startsWith('RTY')) return 13;
235
+ return 15;
236
+ }
237
+
238
+ // Micro E-mini indices (MNQ, MES, MYM, M2K)
239
+ if (name.match(/^(MNQ|MES|MYM|M2K)/)) {
240
+ if (name.startsWith('MNQ')) return 20;
241
+ if (name.startsWith('MES')) return 21;
242
+ if (name.startsWith('MYM')) return 22;
243
+ if (name.startsWith('M2K')) return 23;
244
+ return 25;
245
+ }
246
+
247
+ // Energy (CL, MCL, NG)
248
+ if (name.match(/^(CL|MCL|NG|QG)/)) return 30;
249
+
250
+ // Metals (GC, MGC, SI)
251
+ if (name.match(/^(GC|MGC|SI|HG|PL)/)) return 40;
252
+
253
+ // Currencies (6E, 6B, etc)
254
+ if (name.match(/^(6E|6B|6J|6A|6C|M6E)/)) return 50;
255
+
256
+ // Treasuries (ZB, ZN, ZF, ZT)
257
+ if (name.match(/^(ZB|ZN|ZF|ZT)/)) return 60;
258
+
259
+ // Everything else
260
+ return 100;
261
+ };
262
+
225
263
  symbolChoices.sort((a, b) => {
264
+ const priorityA = getSymbolPriority(a.value);
265
+ const priorityB = getSymbolPriority(b.value);
266
+
267
+ if (priorityA !== priorityB) {
268
+ return priorityA - priorityB;
269
+ }
270
+
271
+ // Same priority - sort alphabetically
226
272
  const aCode = a.value.name || a.value.symbol || '';
227
273
  const bCode = b.value.name || b.value.symbol || '';
228
274
  return aCode.localeCompare(bCode);
@@ -427,6 +473,7 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
427
473
  success: chalk.green,
428
474
  signal: chalk.yellow.bold,
429
475
  trade: chalk.green.bold,
476
+ loss: chalk.magenta.bold,
430
477
  error: chalk.red,
431
478
  warning: chalk.yellow
432
479
  };
@@ -453,10 +500,12 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
453
500
  if (logs.length > MAX_LOGS) logs.shift(); // Remove oldest from top
454
501
  };
455
502
 
456
- // 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
457
505
  const printLog = (type, message) => {
458
506
  addLog(type, message);
459
- displayUI();
507
+ // Don't call displayUI() here - let the spinner interval handle it
508
+ // This prevents flickering when logs arrive rapidly
460
509
  };
461
510
 
462
511
  // Check market hours
@@ -481,19 +530,38 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
481
530
  };
482
531
 
483
532
  // Display full UI with logs (newest first at top)
484
- // Use ANSI escape codes to avoid flickering
485
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
+
486
546
  const displayUI = () => {
487
- // 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
+
488
554
  if (firstDraw) {
489
- console.clear();
490
- 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
491
559
  firstDraw = false;
492
- } else {
493
- // Move cursor to top-left without clearing (avoids flicker)
494
- process.stdout.write('\x1B[H');
495
560
  }
496
561
 
562
+ // Move cursor to home position
563
+ screenBuffer += '\x1B[H';
564
+
497
565
  // Stats
498
566
  const pnlColor = stats.pnl >= 0 ? chalk.green : chalk.red;
499
567
  const pnlStr = (stats.pnl >= 0 ? '+$' : '-$') + Math.abs(stats.pnl).toFixed(2);
@@ -530,25 +598,25 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
530
598
  return text + ' '.repeat(width - text.length);
531
599
  };
532
600
 
533
- console.log();
534
- console.log(chalk.cyan(TOP));
601
+ printLine('');
602
+ printLine(chalk.cyan(TOP));
535
603
  // Logo = 87 chars cyan + 9 chars yellow = 96 total
536
- console.log(chalk.cyan(V) + chalk.cyan(' ██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗') + chalk.yellow('██╗ ██╗') + ' ' + chalk.cyan(V));
537
- console.log(chalk.cyan(V) + chalk.cyan(' ██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝') + chalk.yellow('╚██╗██╔╝') + ' ' + chalk.cyan(V));
538
- console.log(chalk.cyan(V) + chalk.cyan(' ███████║█████╗ ██║ ██║██║ ███╗█████╗ ██║ ██║██║ ██║███████║██╔██╗ ██║ ██║ ') + chalk.yellow(' ╚███╔╝ ') + ' ' + chalk.cyan(V));
539
- console.log(chalk.cyan(V) + chalk.cyan(' ██╔══██║██╔══╝ ██║ ██║██║ ██║██╔══╝ ██║▄▄ ██║██║ ██║██╔══██║██║╚██╗██║ ██║ ') + chalk.yellow(' ██╔██╗ ') + ' ' + chalk.cyan(V));
540
- console.log(chalk.cyan(V) + chalk.cyan(' ██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ') + chalk.yellow('██╔╝ ██╗') + ' ' + chalk.cyan(V));
541
- console.log(chalk.cyan(V) + chalk.cyan(' ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ') + chalk.yellow('╚═╝ ╚═╝') + ' ' + chalk.cyan(V));
542
- 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));
543
611
 
544
612
  // Centered title
545
613
  const title1 = `Prop Futures Algo Trading v${version}`;
546
- console.log(chalk.cyan(V) + chalk.white(center(title1, W)) + chalk.cyan(V));
547
- console.log(chalk.cyan(MID));
614
+ printLine(chalk.cyan(V) + chalk.white(center(title1, W)) + chalk.cyan(V));
615
+ printLine(chalk.cyan(MID));
548
616
 
549
617
  // Centered subtitle
550
618
  const title2 = 'HQX Ultra-Scalping Algorithm';
551
- 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));
552
620
 
553
621
  // Grid layout for metrics - 2 columns per row, 4 rows
554
622
  // Row 1: Account | Symbol + Qty
@@ -598,15 +666,15 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
598
666
  const GRID_BOT = '\u2560' + '\u2550'.repeat(colL) + '\u2567' + '\u2550'.repeat(colR) + '\u2563';
599
667
 
600
668
  // Print grid
601
- console.log(chalk.cyan(GRID_TOP));
602
- console.log(chalk.cyan(V) + r1c1.padded + chalk.cyan(VS) + r1c2 + chalk.cyan(V));
603
- console.log(chalk.cyan(GRID_MID));
604
- console.log(chalk.cyan(V) + r2c1.padded + chalk.cyan(VS) + r2c2.padded + chalk.cyan(V));
605
- console.log(chalk.cyan(GRID_MID));
606
- console.log(chalk.cyan(V) + r3c1.padded + chalk.cyan(VS) + r3c2.padded + chalk.cyan(V));
607
- console.log(chalk.cyan(GRID_MID));
608
- console.log(chalk.cyan(V) + r4c1 + chalk.cyan(VS) + r4c2.padded + chalk.cyan(V));
609
- 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));
610
678
 
611
679
  // Activity log header with spinner and centered date
612
680
  spinnerFrame = (spinnerFrame + 1) % spinnerChars.length;
@@ -621,19 +689,42 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
621
689
  const datePad = Math.max(0, Math.floor((midSpace - dateCentered.length) / 2));
622
690
  const remainingPad = Math.max(0, midSpace - datePad - dateCentered.length);
623
691
  const dateSection = ' '.repeat(datePad) + chalk.cyan(dateCentered) + ' '.repeat(remainingPad);
624
- console.log(chalk.cyan(V) + chalk.white(actLeft) + dateSection + chalk.yellow(actRight) + chalk.cyan(V));
625
- 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, '');
697
+
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
+ };
626
718
 
627
- // Logs (without borders) - newest first, show more logs
628
- // Align with left border of rectangle above (║ = 1 char + 1 space)
719
+ // Logs inside the rectangle - newest first, max 50 lines
629
720
  const MAX_VISIBLE_LOGS = 50;
630
- console.log();
631
721
 
632
722
  if (logs.length === 0) {
633
- console.log(chalk.gray(' Waiting for activity...') + '\x1B[K');
634
- // 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
635
726
  for (let i = 0; i < MAX_VISIBLE_LOGS - 1; i++) {
636
- console.log('\x1B[K');
727
+ bufferLine(chalk.cyan(V) + ' '.repeat(W) + chalk.cyan(V));
637
728
  }
638
729
  } else {
639
730
  // Show newest first (reverse), limited to MAX_VISIBLE_LOGS
@@ -641,25 +732,33 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
641
732
  reversedLogs.forEach(log => {
642
733
  const color = typeColors[log.type] || chalk.white;
643
734
  const icon = getIcon(log.type);
644
- // Align with rectangle: 1 space to match content after ║
645
- const logLine = ` [${log.timestamp}] ${icon} ${log.message}`;
646
- 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));
647
740
  });
648
741
  // Fill remaining lines with empty to keep fixed height
649
742
  for (let i = reversedLogs.length; i < MAX_VISIBLE_LOGS; i++) {
650
- console.log('\x1B[K');
743
+ bufferLine(chalk.cyan(V) + ' '.repeat(W) + chalk.cyan(V));
651
744
  }
652
745
  }
653
- // Clear anything below
654
- 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;
655
754
  };
656
755
 
657
- // Spinner interval to refresh UI
756
+ // Spinner interval to refresh UI - 250ms for stability
658
757
  const spinnerInterval = setInterval(() => {
659
- if (algoRunning) {
758
+ if (algoRunning && !isDrawing) {
660
759
  displayUI();
661
760
  }
662
- }, 100);
761
+ }, 250);
663
762
 
664
763
  // Connect to HQX Server
665
764
  const spinner = ora('Authenticating with HQX Server...').start();
@@ -699,7 +798,7 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
699
798
  // Setup event handlers - logs scroll down naturally
700
799
  hqxServer.on('latency', (data) => {
701
800
  latency = data.latency || 0;
702
- displayUI(); // Refresh UI with new latency
801
+ // Don't call displayUI() - spinner interval will refresh
703
802
  });
704
803
 
705
804
  hqxServer.on('log', (data) => {
@@ -730,12 +829,13 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
730
829
  printLog('trade', `Closed +$${data.pnl.toFixed(2)} (${data.reason || 'take_profit'})`);
731
830
  } else {
732
831
  stats.losses++;
733
- 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'})`);
734
833
  }
735
834
  stats.winRate = stats.trades > 0 ? ((stats.wins / stats.trades) * 100).toFixed(1) : '0.0';
736
835
 
737
836
  // Print updated stats
738
- 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)}`);
739
839
 
740
840
  // Check daily target
741
841
  if (stats.pnl >= dailyTarget) {
@@ -759,8 +859,17 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
759
859
  });
760
860
 
761
861
  hqxServer.on('stats', (data) => {
762
- stats = { ...stats, ...data };
763
- // 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;
764
873
  });
765
874
 
766
875
  hqxServer.on('error', (data) => {
@@ -862,8 +971,9 @@ const launchAlgo = async (service, account, contract, numContracts, dailyTarget,
862
971
  // Clear spinner interval
863
972
  clearInterval(spinnerInterval);
864
973
 
865
- // Show cursor again
866
- 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
867
977
 
868
978
  // Stop algo
869
979
  console.log();